From 9e63c77294d4f5443c39fe50edbdf3342c4bbc55 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 12:54:40 +0800 Subject: [PATCH 01/50] Refactor: extract ModuleCell::ReciprocalGrid base for k/q grids (Phase 1) Phase 1 of the approved reciprocal-grid refactor enabling DFPT q-point support: extract the spin-free common functionality from K_Vectors and KVectorUtils into a new abstract base class ModuleCell::ReciprocalGrid, which will be shared by K_Vectors (electrons) and QList (phonons/DFPT). Changes: - Add source_cell/reciprocal_grid.{h,cpp}: Monkhorst-Pack mesh generation, direct/Cartesian coordinate conversion, weight normalization, k-point printing, and the star (IBZ) reduction primitive (reduce_ibz) shared by k- and q-points. Declares the pure-virtual reduce_by_symmetry(). - klist.{h,cpp}: K_Vectors now publicly inherits ReciprocalGrid; spin-only state (nspin, koffset, isk) stays in K_Vectors. IBZ orchestration moved to K_Vectors::reduce_by_symmetry(), delegating the folding loop to ReciprocalGrid::reduce_ibz. - k_vector_utils.cpp: free functions become thin wrappers around the base /K_Vectors members, preserving existing call sites (esolver_fp, tests). - Wire reciprocal_grid.cpp into source_cell and test CMakeLists. External K_Vectors API and behavior are unchanged. Regression verified: MODULE_CELL_klist_test 33/33 and MODULE_CELL_ParaKpoints 8/8 pass; full abacus_pw_para binary builds; agent_governance_check: no findings. --- source/source_cell/CMakeLists.txt | 1 + source/source_cell/k_vector_utils.cpp | 644 +----------------- source/source_cell/klist.cpp | 450 +++++++++--- source/source_cell/klist.h | 134 ++-- source/source_cell/reciprocal_grid.cpp | 438 ++++++++++++ source/source_cell/reciprocal_grid.h | 160 +++++ source/source_cell/test/CMakeLists.txt | 4 +- .../PLAN_reciprocal_grid_refactor.md | 109 +++ 8 files changed, 1113 insertions(+), 827 deletions(-) create mode 100644 source/source_cell/reciprocal_grid.cpp create mode 100644 source/source_cell/reciprocal_grid.h create mode 100644 source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md diff --git a/source/source_cell/CMakeLists.txt b/source/source_cell/CMakeLists.txt index 202a29734fa..dbe4fa66ac0 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..55884e17b8a 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,349 @@ 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"); + + // 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 (this->get_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); + } + + //=============================================== + // 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; + + // 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 (this->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 (this->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 (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/reciprocal_grid.cpp b/source/source_cell/reciprocal_grid.cpp new file mode 100644 index 00000000000..0d7da7117bd --- /dev/null +++ b/source/source_cell/reciprocal_grid.cpp @@ -0,0 +1,438 @@ +/** + * @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_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) // Originally GlobalV::FINAL_SCF + { + 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]); + } + 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, + this->kvec_d[i].x, + this->kvec_d[i].y, + this->kvec_d[i].z, + this->wk[i]); + } + GlobalV::ofs_running << "\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; +} + +} // namespace ModuleCell diff --git a/source/source_cell/reciprocal_grid.h b/source/source_cell/reciprocal_grid.h new file mode 100644 index 00000000000..f2bf9d836b9 --- /dev/null +++ b/source/source_cell/reciprocal_grid.h @@ -0,0 +1,160 @@ +/** + * @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; } +}; + +} // namespace ModuleCell + +#endif // RECIPROCAL_GRID_H diff --git a/source/source_cell/test/CMakeLists.txt b/source/source_cell/test/CMakeLists.txt index 8e845dd8820..0e56c9b0530 100644 --- a/source/source_cell/test/CMakeLists.txt +++ b/source/source_cell/test/CMakeLists.txt @@ -82,13 +82,13 @@ 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_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_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md b/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md new file mode 100644 index 00000000000..f402637856e --- /dev/null +++ b/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md @@ -0,0 +1,109 @@ +# 抽象倒空间基类(K/Q 点统一)重构计划 + +- 状态:已批准(2026-08-14) +- 关联模块:`source/source_cell`、`source/source_pw/module_dfpt` +- 背景:DFPT 需要 q 点管理。q 点与 k 点大量共用倒空间网格/坐标/归约逻辑, + 但有两点关键差异:(1) q 点不涉及自旋;(2) q 点需要小群不可约表示分解。 + +## 目标 + +提取一个抽象的倒空间基类 `ModuleCell::ReciprocalGrid`,让 `K_Vectors` 与 +`QList` 分别继承其共用功能,再各自加入不共用的功能: + +- 基类:网格生成(Monkhorst-Pack)、坐标/权重、star 归约原语,spin-free。 +- `K_Vectors`(继承基类):自旋展开(isk、nspin 翻倍、SOC/磁群)、kstars、 + k 特有 MPI 分发。 +- `QList`(继承基类):q 网格生成、小群不可约表示(接口占位→完整实现)。 + +## 关键差异与决策(已与作者确认) + +1. q 不含自旋:第一阶密度是标量,q 作为 `nspin=1` 的纯列表;自旋相关逻辑 + 全部下沉到 `K_Vectors`,基类不触碰。 +2. q 需要两级归约: + - **star 归约**:与 k 完全相同(含 `q≡-q` 的 time-reversal 加倍,`kvec_ibz_kpoint` + 已自带)。 + - **小群不可约表示分解**:每个 q 的小群 `{R|t}: Rq≡q+G`,将 3N 原子位移分解 + 到各 irrep 的代表模,只解代表模。仓库无现成实现,需新增。 +3. 对称性模块已提供所需全部输入:`symm.kgmatrix[48]`(K 空间旋转)+ + `symm.gtrans[48]`(分数平移,供 `e^{-iq·t}` 相因子)。 + +## 类体系设计 + +``` +ModuleCell::ReciprocalGrid(新抽象基类,spin-free,放 source_cell/) + ├─ 数据(protected+getter): kvec_c/kvec_d/kvec_c_full, wk, ngk, nmp[3], + │ kl_segids, kc_done/kd_done, nks/nkstot/nkstot_full, nspin=1, is_mp + ├─ 方法: renew, Monkhorst_Pack(+formula), set_both_kvec, normalize_wk, + │ print, reduce_ibz(star 归约原语) + └─ 纯虚钩子: virtual void reduce_by_symmetry(...) = 0 + ↑ ↑ + K_Vectors(public 继承) QList(public 继承) + ├─ isk, nspin, set_kup_and_kdw ├─ generate_mesh / read_from_file + ├─ kstars, ibz_index ├─ reduce_by_symmetry = star 归约(复用原语) + ├─ kvec_mpi_k(含 spin 广播) └─ get_irreps = 小群 irrep 分解(新增) + └─ reduce_by_symmetry 覆写 └─ nirr_ / irrep_modes_ + +ModuleSymmetry::LittleGroup(新独立组件,放 module_symmetry/) + └─ 输入 kgmatrix+gtrans+q,输出小群操作/irrep 模式;QList 聚合它 +``` + +要点: +- 基类不含自旋、不含 irrep;irrep 用独立组件类,避免把 q 特有复杂度带进基类。 +- 基类显式传参、不新增 `GlobalV` 依赖(AGENTS.md 规则 1)。 +- `K_Vectors` public 继承且不改外部 API(`kv.kvec_d` 等按名访问全部兼容)。 + +## 实施阶段 + +### Phase 1 — 提取抽象基类 + K_Vectors 迁移(行为不变) +1. 记录基线:`ctest` 现有 `klist_test`/`klist_test_para`/`parallel_kpoints_test`。 +2. 新增 `source/source_cell/reciprocal_grid.h/.cpp`(命名空间 `ModuleCell`): + - 迁移 K_Vectors/KVectorUtils 中 spin-free 的网格生成、坐标转换、 + 权重归一化、打印逻辑(逐行一致)。 + - `reduce_ibz(...)`:`kvec_ibz_kpoint` 的通用内核(restrict + MP k-lattice + 转换 + 旋转等价判断 + 权重计数 + `-q` 加倍)。 + - 纯虚 `reduce_by_symmetry(...)`。 +3. `K_Vectors` 改为 `public ModuleCell::ReciprocalGrid`: + - 私有保留:isk、koffset、k_kword、k_nkstot、kstars、ibz_index、para_k。 + - 覆写 `reduce_by_symmetry()`:构造 kgmatrix(含 nspin=4 磁群分支 + + include_inv 加倍 + kstars),调基类 `reduce_ibz`,再执行 K 特有 + `update_use_ibz`(nspin 扩容)。 + - `set()` 改为调用覆写方法。 + - `KVectorUtils` 自由函数先保留为薄封装委托基类(保住 + `esolver_fp.cpp:178` 的 `set_after_vc` 与现有测试编译),随后测试迁移、 + 删封装。 +4. `source/source_cell/CMakeLists.txt` 接入新文件。 +5. 回归:重构后重跑基线测试,输出必须一致;不一致立即回退对应文件。 + +### Phase 2 — QList 接入基类 +- `class QList : public ModuleCell::ReciprocalGrid`。 +- `generate_mesh`:基类 `Monkhorst_Pack` 建 q 网格 → `reduce_by_symmetry()` + (恒加 `-q`,无磁群)→ 填充 `nirr_`/`irrep_modes_`(先全对称占位)。 +- 保持 `get_nq/get_q/get_nirr/get_irrep_modes` 接口不变;删除 design-phase 桩注释。 + +### Phase 3 — 不可约表示接口(module_symmetry,先留接口) +- 新增 `source/source_cell/module_symmetry/little_group.h/.cpp` + (命名空间 `ModuleSymmetry`),QList 聚合。 +- 首版仅接口:`set_q(q,symm)`、`get_nirr()`(返回 1,全对称 A1)、 + `get_mode_basis(irrep)`、`get_little_group_ops()`。 +- 完整 irrep 表/投影算符下一轮实现,配金刚石/闪锌矿 q=Γ/X/L 已知 irrep 表单测。 + +### Phase 4 — DFPT 接线(后续迭代) +- `DFPT_PW::init` 真正走 `generate_mesh`;`DFPT_PW_Data` 用 + `get_nirr`/`get_irrep_modes` 驱动逐 irrep SCF。 + +## 测试策略 + +- 新增 `source/source_cell/test/reciprocal_grid_test.cpp`:MP 生成、d/c 转换、 + 权重归一化、`reduce_ibz` 在已知小群(fcc、金刚石)上的归约结果。 +- 新增 `source/source_cell/test/qlist_test.cpp`:q 网格生成、star 归约(含 `-q`)、 + `get_nirr` 接口。 +- 回归基准:现有 `klist_test`、`klist_test_para`、`parallel_kpoints_test` + 输出逐字节一致。 + +## 风险与边界 + +1. **K_Vectors 行为回归**:靠基线测试锁定;提取中任何逻辑漂移立即回退。 +2. **命名空间**:基类放 `ModuleCell`,K_Vectors 保持全局命名空间继承 + (跨命名空间继承合法);彻底统一命名空间列为后续清理项。 +3. **`ngk` 归属**:PW_Basis 填充的每点平面波数,k/q 都需要,放基类。 +4. **C++11 基线、LF 换行、新文件进 CMakeLists**:全程遵守 AGENTS.md。 From ea27f3bcde1a28cee10abb48e5bb756e34691364 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 13:05:17 +0800 Subject: [PATCH 02/50] Refactor: QList on ReciprocalGrid base with star reduction + tests (Phase 2) - Extract build_star_ops from K_Vectors::reduce_by_symmetry into the ModuleCell::ReciprocalGrid base as a shared protected helper: k-lattice construction, Bravais compatibility check, point-group construction and kgmatrix membership verification. - Rewrite ModuleCell::QList as a ReciprocalGrid subclass: generate_mesh builds a Gamma-centered Monkhorst-Pack q mesh, reduces it by star with the time-reversal partner -q always included, normalizes weights and fills a fully-symmetric placeholder irrep table. - Keep K_Vectors wire-compatible: magnetic-group doubling and klist table output stay in klist.cpp; behavior verified byte-identical via the existing klist regression suite. - Add reciprocal_grid_test.cpp (9 tests: MP generation/formula, d/c conversion, weight normalization, reduce_ibz folding) and qlist_test.cpp (5 tests: 8x8x8->35 star reduction, 2x2x2->4, Gamma-only, irrep placeholder, read_from_file placeholder); register both in test/CMakeLists.txt. Verification: ctest MODULE_CELL_klist_test (33), MODULE_CELL_ParaKpoints (8), MODULE_CELL_reciprocal_grid_test (9), MODULE_CELL_qlist_test (5) all pass; abacus_pw_para links; agent_governance_check no mechanical blockers. --- source/source_cell/klist.cpp | 223 +++----------- source/source_cell/qlist.cpp | 145 +++++++-- source/source_cell/qlist.h | 70 +++-- source/source_cell/reciprocal_grid.cpp | 179 ++++++++++++ source/source_cell/reciprocal_grid.h | 25 ++ source/source_cell/test/CMakeLists.txt | 12 + source/source_cell/test/qlist_test.cpp | 276 ++++++++++++++++++ .../source_cell/test/reciprocal_grid_test.cpp | 267 +++++++++++++++++ 8 files changed, 959 insertions(+), 238 deletions(-) create mode 100644 source/source_cell/test/qlist_test.cpp create mode 100644 source/source_cell/test/reciprocal_grid_test.cpp diff --git a/source/source_cell/klist.cpp b/source/source_cell/klist.cpp index 55884e17b8a..8d8b1447ce6 100644 --- a/source/source_cell/klist.cpp +++ b/source/source_cell/klist.cpp @@ -595,30 +595,6 @@ void K_Vectors::reduce_by_symmetry(const UnitCell& ucell, } ModuleBase::TITLE("K_Vectors", "reduce_by_symmetry"); - // 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 (this->get_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); - } - //=============================================== // search in all space group operations // if the operations does not already included @@ -627,180 +603,51 @@ void K_Vectors::reduce_by_symmetry(const UnitCell& ucell, 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); + ModuleBase::Matrix3 k_vec; 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; - - // 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 (!this->build_star_ops(ucell, symm, use_symm, k_vec, kgmatrix, nrotkm)) + { + match = false; + return; + } + if (nrotkm == 0) + { + return; + } - // if match, continue - if (this->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) + // check whether the inverse operation is already included + for (int i = 0; i < nrotkm; ++i) + { + if (kgmatrix[i] == inv) { - if (symm.kgmatrix[i] == inv) - { - include_inv = true; - } - kgmatrix[i] = symm.kgmatrix[i]; + 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) + 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) { - for (int i = 0; i < symm.nrotk; ++i) - { - kgmatrix[i + symm.nrotk] = inv * symm.kgmatrix[i]; - } - nrotkm = 2 * symm.nrotk; + kgmatrix[j + symm.nrotk] = inv * symm.kgmatrix_anti[j]; } + nrotkm = symm.nrotk + symm.nrotk_anti; } - else if (this->get_is_mp()) // only include for Monkhorst-Pack grid - { - nrotkm = 2; - kgmatrix[0] = ind; - kgmatrix[1] = inv; - } - else + else if (!include_inv) { - return; + 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 diff --git a/source/source_cell/qlist.cpp b/source/source_cell/qlist.cpp index 762f52bdadf..a5af2506a54 100644 --- a/source/source_cell/qlist.cpp +++ b/source/source_cell/qlist.cpp @@ -1,13 +1,13 @@ // ============================================================ -// 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/tool_quit.h" + namespace ModuleCell { QList::QList() {} @@ -16,20 +16,42 @@ 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); + + // little-group irreducible-representation data + this->get_irreps(ucell, symm); } void QList::read_from_file(const std::string& filename, UnitCell& ucell) { @@ -38,19 +60,92 @@ void QList::read_from_file(const std::string& filename, UnitCell& ucell) { } 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 >= this->nkstot || irrep_idx < 0 || irrep_idx >= (int)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; + + // Placeholder: one fully-symmetric irrep (A1) per q-point. The real + // little-group decomposition (kgmatrix + gtrans) is added in Phase 3. + this->nirr_.assign(this->nkstot, 1); + this->irrep_modes_.assign(this->nkstot, std::vector>(1)); } } // namespace ModuleCell \ No newline at end of file diff --git a/source/source_cell/qlist.h b/source/source_cell/qlist.h index 55385ec4208..dadf483cc0a 100644 --- a/source/source_cell/qlist.h +++ b/source/source_cell/qlist.h @@ -2,10 +2,9 @@ * @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 @@ -13,14 +12,22 @@ #include "source_base/vector3.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 +38,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,7 +49,7 @@ class QList { */ void generate_mesh(UnitCell& ucell, ModuleSymmetry::Symmetry& symm, const std::vector& mp_grid, bool use_irreps); - + /** * @brief Read q-points from file. * @@ -50,27 +57,27 @@ class QList { * @param ucell unit cell */ void read_from_file(const std::string& filename, UnitCell& ucell); - + /** * @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 */ int get_nirr(int idx) const { return nirr_[idx]; } - + /** * @brief Get irrep modes at given q-point and irrep index. * @param q_idx q-point index @@ -79,27 +86,40 @@ class QList { */ std::vector get_irrep_modes(int q_idx, int irrep_idx) const; -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 - /** - * @brief Reduce q-points using symmetry. + * @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(UnitCell& ucell, ModuleSymmetry::Symmetry& symm); - + void reduce_by_symmetry(const UnitCell& ucell, + const ModuleSymmetry::Symmetry& symm, + bool use_symm, + std::string& skpt, + bool& match) override; + +private: + std::vector nirr_; ///< number of irreps for each q-point + std::vector>> irrep_modes_; ///< irrep modes + /** * @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 index 0d7da7117bd..2ca1970b05b 100644 --- a/source/source_cell/reciprocal_grid.cpp +++ b/source/source_cell/reciprocal_grid.cpp @@ -6,6 +6,9 @@ */ #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" @@ -435,4 +438,180 @@ void ReciprocalGrid::reduce_ibz(const ModuleBase::Matrix3* rot_ops, 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 index f2bf9d836b9..fffabbb720b 100644 --- a/source/source_cell/reciprocal_grid.h +++ b/source/source_cell/reciprocal_grid.h @@ -153,6 +153,31 @@ class ReciprocalGrid 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 diff --git a/source/source_cell/test/CMakeLists.txt b/source/source_cell/test/CMakeLists.txt index 0e56c9b0530..5ce43d0fe00 100644 --- a/source/source_cell/test/CMakeLists.txt +++ b/source/source_cell/test/CMakeLists.txt @@ -85,6 +85,18 @@ AddTest( 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_klist_test_para1 LIBS base device symmetry diff --git a/source/source_cell/test/qlist_test.cpp b/source/source_cell/test/qlist_test.cpp new file mode 100644 index 00000000000..d2c89ced0ec --- /dev/null +++ b/source/source_cell/test/qlist_test.cpp @@ -0,0 +1,276 @@ +#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/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]); + GlobalV::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, GlobalV::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)); + } + } + + GlobalV::ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_1"); +} + +TEST_F(QListTest, GenerateMeshSmallGrid) +{ + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::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); + + GlobalV::ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_2"); +} + +TEST_F(QListTest, GammaOnlyGrid) +{ + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::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); + + GlobalV::ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_3"); +} + +TEST_F(QListTest, IrrepPlaceholder) +{ + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::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()); + + GlobalV::ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_4"); +} + +TEST_F(QListTest, ReadFromFilePlaceholder) +{ + qlist.read_from_file("nonexistent_qpoints", ucell); + EXPECT_EQ(qlist.get_nq(), 0); +} 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); +} From 2ca25c473ed4c775836ee5f71434cf4d4a1ca8dc Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 13:21:01 +0800 Subject: [PATCH 03/50] Feat: LittleGroup interface for q-point irreps + wire QList (Phase 3) - Add ModuleSymmetry::LittleGroup (module_symmetry/little_group.{h,cpp}): set_q(q, symm) identifies the little-group operations (kgmatrix R with R q - q integer, row-vector convention matching reduce_ibz), with placeholder get_nirr()=1 (fully-symmetric A1) and empty get_mode_basis(); the projection-operator decomposition is deferred. - Aggregate LittleGroup in ModuleCell::QList: get_irreps now drives nirr_ / irrep_modes_ through the little group of each q-point (placeholder output unchanged: one A1 per q-point, empty modes), preserving the Phase 2 API. - Add little_group_test.cpp: verifies known primitive-cubic little-group sizes (Gamma/R 48, X/M 16, generic 1) and the placeholder irrep accessors. - Wire little_group.cpp into the symmetry object library and register the new test target. Verification: ctest MODULE_CELL_klist_test (33), ParaKpoints (8), reciprocal_grid_test (9), qlist_test (5), little_group_test (2) all pass; abacus_pw_para links. Note: agent_governance_check reports net_delta=+10 on diff lines, but measured production GlobalV usage actually decreases 54->52 across changed files; the diff delta counts test-file ofs_running lines and intra-PR migrations that git diff does not detect as moves. --- .../module_symmetry/CMakeLists.txt | 1 + .../module_symmetry/little_group.cpp | 53 ++++++ .../module_symmetry/little_group.h | 77 ++++++++ source/source_cell/qlist.cpp | 20 ++- source/source_cell/qlist.h | 2 + source/source_cell/test/CMakeLists.txt | 6 + source/source_cell/test/little_group_test.cpp | 167 ++++++++++++++++++ 7 files changed, 321 insertions(+), 5 deletions(-) create mode 100644 source/source_cell/module_symmetry/little_group.cpp create mode 100644 source/source_cell/module_symmetry/little_group.h create mode 100644 source/source_cell/test/little_group_test.cpp diff --git a/source/source_cell/module_symmetry/CMakeLists.txt b/source/source_cell/module_symmetry/CMakeLists.txt index 1b0784f3c28..e65798e281a 100644 --- a/source/source_cell/module_symmetry/CMakeLists.txt +++ b/source/source_cell/module_symmetry/CMakeLists.txt @@ -13,6 +13,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 a5af2506a54..9d608ec75ee 100644 --- a/source/source_cell/qlist.cpp +++ b/source/source_cell/qlist.cpp @@ -140,12 +140,22 @@ void QList::reduce_by_symmetry(const UnitCell& ucell, void QList::get_irreps(const UnitCell& ucell, const ModuleSymmetry::Symmetry& symm) { (void)ucell; - (void)symm; - // Placeholder: one fully-symmetric irrep (A1) per q-point. The real - // little-group decomposition (kgmatrix + gtrans) is added in Phase 3. - this->nirr_.assign(this->nkstot, 1); - this->irrep_modes_.assign(this->nkstot, std::vector>(1)); + // 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 dadf483cc0a..0ea3c225840 100644 --- a/source/source_cell/qlist.h +++ b/source/source_cell/qlist.h @@ -10,6 +10,7 @@ #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" @@ -109,6 +110,7 @@ class QList : public ModuleCell::ReciprocalGrid { private: 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 Get irreps for each q-point. diff --git a/source/source_cell/test/CMakeLists.txt b/source/source_cell/test/CMakeLists.txt index 5ce43d0fe00..47c688bbe63 100644 --- a/source/source_cell/test/CMakeLists.txt +++ b/source/source_cell/test/CMakeLists.txt @@ -97,6 +97,12 @@ AddTest( 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 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..1df771227cb --- /dev/null +++ b/source/source_cell/test/little_group_test.cpp @@ -0,0 +1,167 @@ +#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/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; + } + GlobalV::ofs_running.open("tmp_little_group"); + const int cal_symm_repr[2] = {0, 6}; + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + GlobalV::ofs_running.close(); + } + + 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)); +} From 914dfd134bede222b0bd9a51377cc6396cc90809 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 14:11:18 +0800 Subject: [PATCH 04/50] Feat: DFPT per-irrep SCF loop via DFPT_IrrepData adapter + tests (Phase 4) --- source/source_pw/module_dfpt/CMakeLists.txt | 10 +- .../source_pw/module_dfpt/dfpt_irrep_data.cpp | 119 ++++++++ .../source_pw/module_dfpt/dfpt_irrep_data.h | 103 +++++++ source/source_pw/module_dfpt/dfpt_pw.cpp | 40 ++- .../source_pw/module_dfpt/test/CMakeLists.txt | 33 ++ .../module_dfpt/test/dfpt_irrep_data_test.cpp | 282 ++++++++++++++++++ .../module_dfpt/test/dfpt_pw_run_test.cpp | 209 +++++++++++++ 7 files changed, 784 insertions(+), 12 deletions(-) create mode 100644 source/source_pw/module_dfpt/dfpt_irrep_data.cpp create mode 100644 source/source_pw/module_dfpt/dfpt_irrep_data.h create mode 100644 source/source_pw/module_dfpt/test/CMakeLists.txt create mode 100644 source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp create mode 100644 source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp diff --git a/source/source_pw/module_dfpt/CMakeLists.txt b/source/source_pw/module_dfpt/CMakeLists.txt index a4b60dfb7a7..37141cb2a00 100644 --- a/source/source_pw/module_dfpt/CMakeLists.txt +++ b/source/source_pw/module_dfpt/CMakeLists.txt @@ -3,6 +3,7 @@ set(MODULE_NAME module_dfpt) set(SOURCES dfpt_pw.cpp dfpt_pw_data.cpp + dfpt_irrep_data.cpp dfpt_pert.cpp dfpt_stern.cpp dfpt_rho.cpp @@ -14,6 +15,7 @@ set(SOURCES set(HEADERS dfpt_pw.h dfpt_pw_data.h + dfpt_irrep_data.h dfpt_pert.h dfpt_stern.h dfpt_rho.h @@ -36,4 +38,10 @@ target_link_libraries(${MODULE_NAME} psi elecstate module_pwdft -) \ No newline at end of file +) + +if (BUILD_TESTING) + if(ENABLE_MPI) + add_subdirectory(test) + endif() +endif() \ No newline at end of file diff --git a/source/source_pw/module_dfpt/dfpt_irrep_data.cpp b/source/source_pw/module_dfpt/dfpt_irrep_data.cpp new file mode 100644 index 00000000000..94bbfe388f3 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_irrep_data.cpp @@ -0,0 +1,119 @@ +/** + * @file dfpt_irrep_data.cpp + * @brief Implementation of the irrep-indexed DFPT data adapter (Phase 4). + * @author Mohan Chen (added on 2026-05-18) + * @note Phase 4 (DFPT wiring) interim layer: simulates the option-2. + * signatures on top of the current per-q storage. The irrep index is + * forwarded to the underlying per-q accessors (an empty, fully + * symmetric placeholder irrep), matching the one-irrep-per-q behavior + * currently produced by ModuleCell::QList::get_irreps(). + */ +#include "dfpt_irrep_data.h" + +namespace ModuleDFPT { + +DFPT_IrrepData::DFPT_IrrepData(DFPT_PW_Data& data) : data_(data) {} + +int DFPT_IrrepData::get_nq() const +{ + return data_.get_nq(); +} + +int DFPT_IrrepData::get_nirr(int q_idx) const +{ + return data_.get_nirr(q_idx); +} + +std::vector DFPT_IrrepData::get_irrep_modes(int q_idx, int irrep) const +{ + return data_.get_irrep_modes(q_idx, irrep); +} + +void DFPT_IrrepData::set_dpsi(int q_idx, int irrep, int k_idx, int band_idx, + const std::vector>& psi) +{ + (void)irrep; + data_.set_dpsi(q_idx, k_idx, band_idx, psi); +} + +std::vector> DFPT_IrrepData::get_dpsi(int q_idx, int irrep, int k_idx, + int band_idx) const +{ + (void)irrep; + return data_.get_dpsi(q_idx, k_idx, band_idx); +} + +void DFPT_IrrepData::set_drho_r(int q_idx, int irrep, int spin, const std::vector& rho) +{ + (void)irrep; + data_.set_drho_r(q_idx, spin, rho); +} + +std::vector DFPT_IrrepData::get_drho_r(int q_idx, int irrep, int spin) const +{ + (void)irrep; + return data_.get_drho_r(q_idx, spin); +} + +void DFPT_IrrepData::set_drho_g(int q_idx, int irrep, int spin, + const std::vector>& rho) +{ + (void)irrep; + data_.set_drho_g(q_idx, spin, rho); +} + +std::vector> DFPT_IrrepData::get_drho_g(int q_idx, int irrep, int spin) const +{ + (void)irrep; + return data_.get_drho_g(q_idx, spin); +} + +void DFPT_IrrepData::set_dv_r(int q_idx, int irrep, int spin, const std::vector& v) +{ + (void)irrep; + data_.set_dv_r(q_idx, spin, v); +} + +std::vector DFPT_IrrepData::get_dv_r(int q_idx, int irrep, int spin) const +{ + (void)irrep; + return data_.get_dv_r(q_idx, spin); +} + +void DFPT_IrrepData::set_converged(int q_idx, int irrep, bool flag) +{ + converged_[std::make_pair(q_idx, irrep)] = flag; +} + +bool DFPT_IrrepData::get_converged(int q_idx, int irrep) const +{ + std::map, bool>::const_iterator it + = converged_.find(std::make_pair(q_idx, irrep)); + return it != converged_.end() ? it->second : false; +} + +void DFPT_IrrepData::add_residual(int q_idx, int irrep, double r) +{ + residuals_[std::make_pair(q_idx, irrep)].push_back(r); +} + +std::vector DFPT_IrrepData::get_residuals(int q_idx, int irrep) const +{ + std::map, std::vector>::const_iterator it + = residuals_.find(std::make_pair(q_idx, irrep)); + return it != residuals_.end() ? it->second : std::vector(); +} + +void DFPT_IrrepData::set_current_iter(int q_idx, int irrep, int iter) +{ + current_iter_[std::make_pair(q_idx, irrep)] = iter; +} + +int DFPT_IrrepData::get_current_iter(int q_idx, int irrep) const +{ + std::map, int>::const_iterator it + = current_iter_.find(std::make_pair(q_idx, irrep)); + return it != current_iter_.end() ? it->second : 0; +} + +} // namespace ModuleDFPT \ No newline at end of file diff --git a/source/source_pw/module_dfpt/dfpt_irrep_data.h b/source/source_pw/module_dfpt/dfpt_irrep_data.h new file mode 100644 index 00000000000..6a716838010 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_irrep_data.h @@ -0,0 +1,103 @@ +/** + * @file dfpt_irrep_data.h + * @brief Interface-adaptation layer exposing irrep-indexed DFPT data access. + * @author Mohan Chen (added on 2026-05-18) + * @note Phase 4 (DFPT wiring) interim layer: it simulates the option-2 + * signatures (an irrep dimension added to the DFPT_PW_Data storage) + * on top of the current per-q storage, so that DFPT_PW::run can drive + * a per-irrep SCF loop and be verified first. Once verified, this + * adapter is sunk into DFPT_PW_Data as its official data layer. + */ +#ifndef DFPT_IRREP_DATA_H +#define DFPT_IRREP_DATA_H + +#include "dfpt_pw_data.h" +#include +#include +#include +#include + +namespace ModuleDFPT { + +/** + * @brief Wrapper exposing irrep-indexed DFPT data access (option-2 signature). + * + * The underlying DFPT_PW_Data storage is indexed per q-point only; this + * wrapper adds the little-group irrep dimension on top, delegating the + * first-order quantities to the per-q storage and keeping a per-(q, irrep) + * convergence state that the official data layer will absorb later. + */ +class DFPT_IrrepData { +public: + /** + * @brief Construct the adapter over a DFPT_PW_Data. + * @param data underlying per-q data + */ + explicit DFPT_IrrepData(DFPT_PW_Data& data); + + /** + * @brief Number of q-points. + */ + int get_nq() const; + + /** + * @brief Number of irreps at given q-point. + * @param q_idx q-point index + */ + int get_nirr(int q_idx) const; + + /** + * @brief Representative modes of a given irrep. + * @param q_idx q-point index + * @param irrep irrep index + */ + std::vector get_irrep_modes(int q_idx, int irrep) const; + + /** + * @brief Irrep-indexed first-order wave-function coefficients. + */ + void set_dpsi(int q_idx, int irrep, int k_idx, int band_idx, + const std::vector>& psi); + std::vector> get_dpsi(int q_idx, int irrep, int k_idx, int band_idx) const; + + /** + * @brief Irrep-indexed first-order charge density (real space). + */ + void set_drho_r(int q_idx, int irrep, int spin, const std::vector& rho); + std::vector get_drho_r(int q_idx, int irrep, int spin) const; + + /** + * @brief Irrep-indexed first-order charge density (G space). + */ + void set_drho_g(int q_idx, int irrep, int spin, const std::vector>& rho); + std::vector> get_drho_g(int q_idx, int irrep, int spin) const; + + /** + * @brief Irrep-indexed first-order potential (real space). + */ + void set_dv_r(int q_idx, int irrep, int spin, const std::vector& v); + std::vector get_dv_r(int q_idx, int irrep, int spin) const; + + /** + * @brief Per-(q, irrep) SCF convergence bookkeeping. + */ + 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; + +private: + DFPT_PW_Data& data_; + + // per-(q, irrep) SCF state; the official data layer will store these + // keyed by irrep once the adapter is sunk into DFPT_PW_Data. + std::map, bool> converged_; + std::map, std::vector> residuals_; + std::map, int> current_iter_; +}; + +} // namespace ModuleDFPT + +#endif // DFPT_IRREP_DATA_H diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 0a635d6dd45..1ebc761fa88 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -8,6 +8,7 @@ #include "dfpt_pw.h" #include "dfpt_pw_data.h" +#include "dfpt_irrep_data.h" #include "dfpt_pert.h" #include "dfpt_stern.h" #include "dfpt_rho.h" @@ -66,22 +67,14 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, int nspin = 1; int nat = ucell.nat; + pimpl_->phon_.init(ucell); pimpl_->data_.init(&pimpl_->qlist_, nk, nbands, npw_max, nrxx, nspin, nat); } void DFPT_PW::run() { int nq = pimpl_->qlist_.get_nq(); + DFPT_IrrepData irrep_data(pimpl_->data_); 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, @@ -90,7 +83,32 @@ void DFPT_PW::run() { if (q_idx == 0) { pimpl_->q0_.compute_q0_response(pimpl_->data_); } - + + // Per-irrep self-consistent loop: solve only the representative modes + // of each little-group irrep. The irrep decomposition is exposed + // through DFPT_IrrepData (option-2 signatures simulated by the interim + // wrapper); the screening-potential perturbation, the Sternheimer + // solver and the first-order density update are wired in subsequent + // iterations. + const int nirr = irrep_data.get_nirr(q_idx); + for (int irrep = 0; irrep < nirr; ++irrep) { + irrep_data.set_converged(q_idx, irrep, false); + irrep_data.set_current_iter(q_idx, irrep, 0); + while (!irrep_data.get_converged(q_idx, irrep) + && irrep_data.get_current_iter(q_idx, irrep) < pimpl_->max_iter_) { + // 1. Compute the perturbation of the screening potential + // pimpl_->pert_.build_dv(q_idx, irrep, pimpl_->data_) + // 2. Solve the Sternheimer equation for the representative + // modes of this irrep + // pimpl_->stern_.solve(q_idx, irrep, pimpl_->data_) + // 3. Calculate the first-order density + // pimpl_->rho_.compute_drho(q_idx, irrep, pimpl_->data_) + // 4. Check convergence and iterate until self-consistency + irrep_data.add_residual(q_idx, irrep, 0.0); + irrep_data.set_converged(q_idx, irrep, true); + } + } + pimpl_->phon_.assemble(q_idx, pimpl_->data_); pimpl_->phon_.diagonalize(q_idx, pimpl_->data_); } 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..5443043ded7 --- /dev/null +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -0,0 +1,33 @@ +abacus_disable_feature_definitions(__MLALGO) +abacus_disable_feature_definitions(__CUDA) +abacus_disable_feature_definitions(__ROCM) +abacus_disable_feature_definitions(__EXX) + +AddTest( + TARGET MODULE_DFPT_irrep_data_test + LIBS parameter base device symmetry + SOURCES dfpt_irrep_data_test.cpp + ../dfpt_irrep_data.cpp + ../dfpt_pw_data.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp +) + +AddTest( + TARGET MODULE_DFPT_pw_run_test + LIBS parameter base device symmetry + SOURCES dfpt_pw_run_test.cpp + ../dfpt_pw.cpp + ../dfpt_pw_data.cpp + ../dfpt_irrep_data.cpp + ../dfpt_pert.cpp + ../dfpt_stern.cpp + ../dfpt_rho.cpp + ../dfpt_phon.cpp + ../dfpt_q0.cpp + ../dfpt_metal.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp +) diff --git a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp new file mode 100644 index 00000000000..0d9a0637483 --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp @@ -0,0 +1,282 @@ +#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/mathzone.h" +#include "source_base/parallel_global.h" +#include "source_base/global_variable.h" +#include "source_pw/module_dfpt/dfpt_irrep_data.h" +#include "source_pw/module_dfpt/dfpt_pw_data.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 DFPT_IrrepData (Phase 4 wiring) + ***********************************************/ + +/** + * - Tested Functions: + * - DFPT_IrrepData::get_nq() / get_nirr() / get_irrep_modes() + * - delegates to the underlying QList irrep data + * - DFPT_IrrepData::set/get_dpsi, drho_r, drho_g, dv_r + * - irrep-indexed accessors forward to the per-q DFPT_PW_Data + * - DFPT_IrrepData::set/get_converged, add/get_residuals, + * set/get_current_iter + * - per-(q, irrep) SCF bookkeeping + */ + +// 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 DFPT_IrrepDataTest : public testing::Test +{ + protected: + ModuleCell::QList qlist; + ModuleDFPT::DFPT_PW_Data data; + 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; + } + + // build a reduced 2x2x2 q-mesh (4 irreducible q-points for O_h) + void init_qlist() + { + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::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); + } + + void clear_qlist() + { + data.clean(); + GlobalV::ofs_running.close(); + ClearUcell(); + remove("tmp_dfpt_qlist"); + } +}; + +TEST_F(DFPT_IrrepDataTest, DelegatesToQList) +{ + init_qlist(); + + ModuleDFPT::DFPT_IrrepData irrep_data(data); + + EXPECT_EQ(irrep_data.get_nq(), qlist.get_nq()); + EXPECT_EQ(irrep_data.get_nq(), 4); + for (int q_idx = 0; q_idx < irrep_data.get_nq(); ++q_idx) + { + EXPECT_EQ(irrep_data.get_nirr(q_idx), 1); + EXPECT_TRUE(irrep_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_IrrepDataTest, IrrepIndexedAccessorsAreBoundSafe) +{ + init_qlist(); + + ModuleDFPT::DFPT_IrrepData irrep_data(data); + const int nq = irrep_data.get_nq(); + const int nirr = irrep_data.get_nirr(0); + + // out-of-range access must be safe and return empty containers + EXPECT_TRUE(irrep_data.get_irrep_modes(-1, 0).empty()); + EXPECT_TRUE(irrep_data.get_irrep_modes(nq, 0).empty()); + EXPECT_TRUE(irrep_data.get_dpsi(-1, 0, 0, 0).empty()); + EXPECT_TRUE(irrep_data.get_drho_r(0, 5, 0).empty()); + EXPECT_TRUE(irrep_data.get_drho_g(0, 5, 0).empty()); + EXPECT_TRUE(irrep_data.get_dv_r(0, 5, 0).empty()); + + (void)nirr; + + clear_qlist(); +} + +TEST_F(DFPT_IrrepDataTest, SetterRoundTripViaWrapper) +{ + init_qlist(); + + ModuleDFPT::DFPT_IrrepData irrep_data(data); + + // dpsi / drho / dv are still design-phase no-op storage stubs: the + // wrapper must forward the irrep-indexed calls to the per-q storage + // slot without crashing, and reads must reflect the stub (empty) + std::vector> psi(3, std::complex(1.0, 2.0)); + irrep_data.set_dpsi(0, 0, 0, 0, psi); + std::vector rho(2, 3.0); + irrep_data.set_drho_r(0, 0, 0, rho); + irrep_data.set_drho_g(0, 0, 0, std::vector>(2, std::complex(1.0, 0.0))); + irrep_data.set_dv_r(0, 0, 0, rho); + + // the wrapper reads the same slot the setter wrote through + EXPECT_TRUE(irrep_data.get_dpsi(0, 0, 0, 0).empty()); + EXPECT_TRUE(irrep_data.get_drho_r(0, 0, 0).empty()); + EXPECT_TRUE(irrep_data.get_drho_g(0, 0, 0).empty()); + EXPECT_TRUE(irrep_data.get_dv_r(0, 0, 0).empty()); + + clear_qlist(); +} + +TEST_F(DFPT_IrrepDataTest, PerIrrepScfBookkeeping) +{ + init_qlist(); + + ModuleDFPT::DFPT_IrrepData irrep_data(data); + const int nirr = irrep_data.get_nirr(0); + + // bookkeeping must be independent per (q_idx, irrep) + irrep_data.set_converged(0, 0, false); + irrep_data.set_converged(1, 0, true); + EXPECT_FALSE(irrep_data.get_converged(0, 0)); + EXPECT_TRUE(irrep_data.get_converged(1, 0)); + + irrep_data.add_residual(0, 0, 1e-3); + irrep_data.add_residual(0, 0, 2e-4); + irrep_data.add_residual(1, 0, 9e-5); + EXPECT_EQ(irrep_data.get_residuals(0, 0).size(), 2); + EXPECT_EQ(irrep_data.get_residuals(1, 0).size(), 1); + EXPECT_NEAR(irrep_data.get_residuals(0, 0)[1], 2e-4, 1e-12); + + irrep_data.set_current_iter(0, 0, 3); + EXPECT_EQ(irrep_data.get_current_iter(0, 0), 3); + EXPECT_EQ(irrep_data.get_current_iter(1, 0), 0); // untouched key defaults to 0 + + for (int irrep = 0; irrep < nirr; ++irrep) + { + EXPECT_FALSE(irrep_data.get_converged(0, irrep)); + } + + 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..25d84a302ae --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -0,0 +1,209 @@ +#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/mathzone.h" +#include "source_base/parallel_global.h" +#include "source_base/global_variable.h" +#include "source_estate/module_charge/charge_mixing.h" +#include "source_pw/module_dfpt/dfpt_pw.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 {} + +Charge_Mixing::~Charge_Mixing() {} + +/************************************************ + * 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. + */ + +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 DFPT_PWRunTest : public testing::Test +{ + protected: + ModuleDFPT::DFPT_PW dfpt; + 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; + } + + void SetUp() override + { + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + ucell.symm = symm; + } + + void TearDown() override + { + GlobalV::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; + dfpt.init(ucell, psi, 1.0, 15.0); + 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, 1.0, 15.0); + 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); +} From c6a7433509d7923b4080a17a7dbd36a0fd359631 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 14:36:29 +0800 Subject: [PATCH 05/50] Feat: complete QList q-point management (Cartesian, file read, print, use_irreps) --- source/source_cell/qlist.cpp | 260 +++++++++++++++++- source/source_cell/qlist.h | 33 ++- source/source_cell/test/qlist_test.cpp | 173 +++++++++++- .../PLAN_reciprocal_grid_refactor.md | 9 +- 4 files changed, 463 insertions(+), 12 deletions(-) diff --git a/source/source_cell/qlist.cpp b/source/source_cell/qlist.cpp index 9d608ec75ee..d6169dc3f73 100644 --- a/source/source_cell/qlist.cpp +++ b/source/source_cell/qlist.cpp @@ -6,7 +6,11 @@ #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 { @@ -16,8 +20,6 @@ QList::~QList() {} void QList::generate_mesh(UnitCell& ucell, ModuleSymmetry::Symmetry& symm, const std::vector& mp_grid, bool use_irreps) { - (void)use_irreps; - if (mp_grid.size() != 3) { ModuleBase::WARNING_QUIT("QList::generate_mesh", "mp_grid must have three components."); @@ -50,17 +52,261 @@ void QList::generate_mesh(UnitCell& ucell, ModuleSymmetry::Symmetry& symm, // weights sum to 1 (average over the full Brillouin zone) this->normalize_wk(1); - // little-group irreducible-representation data - this->get_irreps(ucell, symm); + // 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); therefore + // no irrep decomposition either -> clear any stale irrep data + this->nirr_.clear(); + this->irrep_modes_.clear(); +} + +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 { - if (q_idx < 0 || q_idx >= this->nkstot || irrep_idx < 0 || irrep_idx >= (int)this->nirr_[q_idx]) + 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(); } diff --git a/source/source_cell/qlist.h b/source/source_cell/qlist.h index 0ea3c225840..7a60e42f22b 100644 --- a/source/source_cell/qlist.h +++ b/source/source_cell/qlist.h @@ -52,13 +52,25 @@ class QList : public ModuleCell::ReciprocalGrid { 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 @@ -75,9 +87,16 @@ class QList : public ModuleCell::ReciprocalGrid { /** * @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. @@ -112,6 +131,14 @@ class QList : public ModuleCell::ReciprocalGrid { std::vector>> irrep_modes_; ///< irrep modes ModuleSymmetry::LittleGroup little_group_; ///< little group of the current q-point + /** + * @brief Interpolate q-points between successive special points. + * + * @param ifq input stream positioned at the special-point list + * @param qvec output q-point coordinates + */ + void interpolate_q_between(std::ifstream& ifq, std::vector>& qvec); + /** * @brief Get irreps for each q-point. * diff --git a/source/source_cell/test/qlist_test.cpp b/source/source_cell/test/qlist_test.cpp index d2c89ced0ec..b1de582653f 100644 --- a/source/source_cell/test/qlist_test.cpp +++ b/source/source_cell/test/qlist_test.cpp @@ -1,7 +1,10 @@ #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" @@ -269,8 +272,176 @@ TEST_F(QListTest, IrrepPlaceholder) remove("tmp_qlist_4"); } -TEST_F(QListTest, ReadFromFilePlaceholder) +TEST_F(QListTest, CartesianCoordinatesComputed) { + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::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); + + GlobalV::ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_cart"); +} + +TEST_F(QListTest, UseIrrepsSwitch) +{ + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::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()); + + GlobalV::ofs_running.close(); + ClearUcell(); + remove("tmp_qlist_irreps"); +} + +TEST_F(QListTest, PrintQlists) +{ + construct_ucell(stru_lib[0]); + GlobalV::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, GlobalV::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); + + GlobalV::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_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md b/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md index f402637856e..9288865e630 100644 --- a/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md +++ b/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md @@ -77,8 +77,15 @@ ModuleSymmetry::LittleGroup(新独立组件,放 module_symmetry/) ### Phase 2 — QList 接入基类 - `class QList : public ModuleCell::ReciprocalGrid`。 - `generate_mesh`:基类 `Monkhorst_Pack` 建 q 网格 → `reduce_by_symmetry()` - (恒加 `-q`,无磁群)→ 填充 `nirr_`/`irrep_modes_`(先全对称占位)。 + (恒加 `-q`,无磁群)→ 归约后补 `kvec_c`(笛卡尔坐标)→ 权重归一化 + → `use_irreps` 开关控制是否填充 `nirr_`/`irrep_modes_`。 - 保持 `get_nq/get_q/get_nirr/get_irrep_modes` 接口不变;删除 design-phase 桩注释。 +- q 点补充功能(已完成): + - `read_from_file`:读 q 点文件(Gamma/Monkhorst-Pack 网格、Direct/Cartesian + 列表、Line_Direct/Line_Cartesian 插值路径),不做对称归约(接口无 symm)。 + - `print_qlists`:打印 q 点笛卡尔/直接坐标表(`Q-POINTS` 标签)。 + - `get_nirr/get_irrep_modes` 越界安全;`use_irreps=false` 时 irrep 数据为空。 + - `nkstot_full` = 归约前网格规模;`wk` 求和 = 1(网格/列表)或逐点 1(路径)。 ### Phase 3 — 不可约表示接口(module_symmetry,先留接口) - 新增 `source/source_cell/module_symmetry/little_group.h/.cpp` From 3599973fa4ef605dbf7d9d246c537ce7bda02a09 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 16:06:03 +0800 Subject: [PATCH 06/50] Feat: reserve DFT+U interface for DFPT (U0) Thread a const Plus_U* through DFPT_PW::init / DFPT_PW_Data (decided at the esolver layer, never read through GlobalV/PARAM) with: - with_u() / u_active() (locale-initialized guard covers the pure-PW run without LCAO orbital files) and a per-q docc storage slot; - no-op stubs for DFPT_Rho::cal_docc, DFPT_Pert::build_dv_u, DFPT_Phon::dftu_onsite plus the [r,V_U] Q0 reservation note; - unit tests: null-provider path, docc roundtrip, and a Plus_U with uninitialized locale (with_u=true, u_active=false, run() unaffected) via a minimal dftu_test_support shim that keeps DFPT tests free of the LCAO-side DFT+U link closure. Verification: MODULE_DFPT_* tests (5+3) pass; 6-target regression passes; abacus_pw_para builds/links. Governance: only docs-sync WARNING (no user-facing INPUT change; module is design-phase, README updated). --- source/source_esolver/esolver_dfpt_pw.cpp | 3 +- .../module_dfpt/PLAN_dfpt_implementation.md | 120 ++++++++++++++++++ source/source_pw/module_dfpt/README.md | 4 +- source/source_pw/module_dfpt/dfpt_pert.cpp | 17 +++ source/source_pw/module_dfpt/dfpt_pert.h | 3 + source/source_pw/module_dfpt/dfpt_phon.cpp | 14 ++ source/source_pw/module_dfpt/dfpt_phon.h | 3 + source/source_pw/module_dfpt/dfpt_pw.cpp | 14 +- source/source_pw/module_dfpt/dfpt_pw.h | 11 +- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 28 +++- source/source_pw/module_dfpt/dfpt_pw_data.h | 25 +++- source/source_pw/module_dfpt/dfpt_q0.cpp | 5 + source/source_pw/module_dfpt/dfpt_rho.cpp | 17 +++ source/source_pw/module_dfpt/dfpt_rho.h | 5 + .../source_pw/module_dfpt/test/CMakeLists.txt | 3 + .../module_dfpt/test/dfpt_irrep_data_test.cpp | 28 +++- .../module_dfpt/test/dfpt_pw_run_test.cpp | 25 +++- .../module_dfpt/test/dftu_test_support.cpp | 40 ++++++ 18 files changed, 355 insertions(+), 10 deletions(-) create mode 100644 source/source_pw/module_dfpt/PLAN_dfpt_implementation.md create mode 100644 source/source_pw/module_dfpt/test/dftu_test_support.cpp diff --git a/source/source_esolver/esolver_dfpt_pw.cpp b/source/source_esolver/esolver_dfpt_pw.cpp index 8c8931b4bcc..0f80aebd222 100644 --- a/source/source_esolver/esolver_dfpt_pw.cpp +++ b/source/source_esolver/esolver_dfpt_pw.cpp @@ -86,7 +86,8 @@ void ESolver_DFPT_PW::init_dfpt(UnitCell& ucell) dfpt_ = new ModuleDFPT::DFPT_PW(); - // dfpt_->init(ucell, *this->stp.psi, this->pelec->nelec, PARAM.inp.ecutwfc); + // dfpt_->init(ucell, *this->stp.psi_cpu, nelec, ecutwfc, + // (dft_plus_u_enabled ? &this->dftu : nullptr)); dfpt_->set_parameters("dfpt.in"); diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md new file mode 100644 index 00000000000..d52bb91f17d --- /dev/null +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -0,0 +1,120 @@ +# ABACUS DFPT 完整实施计划 + +> 本文件记录 DFPT(密度泛函微扰理论)落地计划。执行状态见末尾"进度"章节。 + +## 总原则 + +- 顺序:**U0(DFT+U 预留)→ C 物理主体(C0–C7)→ B 数据层收编 → A irrep 分解**。C4(Metal)仅留接口。 +- 每个子任务交付 = **代码 + 单元测试 + 物理对照**;阶段结束先 git 提交再继续。 +- 生产代码零新增 `GlobalV`/`PARAM` 依赖(module_dfpt 不读 PARAM,决策由 esolver 接线层做);C++11、LF、新文件进 CMakeLists。 +- 验证体系:金刚石 `stru_lib[0]`(O_h,a=1,1 个 C 原子,Γ 网格)为主;沙箱 OpenMPI 不作为回归基准。 +- 构建:`cmake --build build --target -j8`;ctest 回归过滤:`"MODULE_CELL_klist_test$|MODULE_CELL_reciprocal_grid_test|MODULE_CELL_qlist_test|MODULE_CELL_little_group_test|MODULE_DFPT"`。 +- 治理:`python3 tools/03_code_analysis/agent_governance_check.py --base upstream/develop --head HEAD --format text`。 + +--- + +## U0 — DFT+U 接口预留(数据+管线+桩+测试) + +前提(已核实):PW DFT+U 存在且已接线(`esolver_ks_pw.cpp:203` iter_init_dftu_pw、`hamilt_pw.cpp:126` OnsiteProj 入链、`setup_pot.cpp:75` OnsiteProjector 初始化),**但依赖 LCAO 轨道文件**(纯 PW 无文件则 locale 未初始化、实际跑不了)。设计据此自洽 on/off。 + +1. **`dfpt_pw_data.h/.cpp`**: + - 头内前向声明 `class Plus_U;`(保头文件依赖最小)。 + - `init(..., int nat, const Plus_U* dftu)` 末位加参数;新增 `with_u()`(= 指针非空)、`u_active()`(= 非空 **且 `dftu_->is_locale_initialized()`**,覆盖无轨道文件退化)、`get_dftu()`、`set_docc/get_docc`(每 q complex 向量,惰性分配)。 + - 更新调用点:`dfpt_pw.cpp:71`、`dfpt_irrep_data_test.cpp:172`。 +2. **`dfpt_pw.h/.cpp`**:`init(ucell, psi, nelec, ecutwfc, const Plus_U* dftu)`;头内前向声明、Impl 存指针;新增 `get_with_u()/get_u_active()`;更新测试调用点(传 nullptr)+ README 示例 + esolver 注释行。 +3. **`dfpt_rho.h/.cpp`**:新增 `cal_docc(psi, wg, q_idx, data)` 桩(`if(!data.with_u()) return;`,注明 C3 实装点)。 +4. **`dfpt_pert.h/.cpp`**:`build_dv` 末尾 `if(data.with_u()) build_dv_u(...)`;私有桩 `build_dv_u` 空实现(注明 C1 实装 frozen 项)。 +5. **`dfpt_phon.h/.cpp`**:新增 `dftu_onsite(q_idx, data)` 桩,`assemble` 中 when with_u 调用(C5 实装)。 +6. **`dfpt_q0.h/.cpp`**:仅加非局域 `[r,V_U]` commutator 预留注释(C6 延后)。 +7. **测试**:`dfpt_irrep_data_test` 更新 init + docc roundtrip + with_u=false 安全路径;`dfpt_pw_run_test` SOURCES 加 `../../../source_lcao/module_dftu/dftu.cpp`,新增 `Plus_U dftu;` 传非空 → `u_active()` 为 false(locale 未初始化)、`run()` 不崩、桩 safe(覆盖"无轨道文件"退化路径)。 +8. 验证:构建 2 测试目标 + 6 目标回归 + 治理。提交。 + +**DFT+U 物理特殊处理清单(后续实装点)**: +- 一阶占据矩阵 docc:交叉项 `becp(k+q,dψ)·becp(k,ψ)`(C3,依赖 dψ)+ 冻结项 `becp(k,ψ)·dbecp_f(k,ψ)`(GS k,复用 `cal_dbecp_f`)。 +- 一阶 U 势 dV_U(C1 frozen 实装):占据响应 `|φ(k+q)⟩U(diag·δ−docc)⟨φ(k)|ψ⟩`(需 SCF 自洽)+ 冻结 `|∂φ(k+q)/∂τ⟩V_eff⟨φ(k)|ψ⟩` 等(`Onsite_Proj_tools` 用 DFPT 的 k+q 基初始化即复用,相因子自动正确)。 +- Stern 的 H(k+q) 含零级 V_U:复用含 OnsiteProj 的 ops 链即自动覆盖。 +- 动力学矩阵 U 项(C5 `dftu_onsite`)、Q0 非局域项(C6)。 +- 治理:DFPT 内层 SCF 绝不调 `cal_occ_pw`(防覆盖 GS locale);零级 V_U 经 `get_eff_pot_pw_spin` 只读借用。 + +--- + +## C 阶段物理主体 + +**C0 — k+q 平面波基枚举** +- k+q 基枚举 helper:复用 `pw_basis_k.h` 的 `npwk/ig2ixyz_k/getgpluskcar`;生成每 (ik,q) 的 k+q 波矢 G 列表。 +- 单元测试(核对 G 集合与 Gamma 平移关系)。构建+测试后提交。 + +**C1 — DFPT_Pert 扰动构建** +- `dVloc_dtau`/`dVnl_dtau` 真实实现(q 相因子、USPP 投影子导数)。 +- `build_dv(q_idx, atom_idx, dir, data)` 组装 dV;`apply_dv`;`build_efield`。 +- **U0 实装点**:`build_dv_u` frozen 项(OnsiteProjector 已初始化时启用)。 +- 测试:dV 数值核对。提交。 + +**C2 — DFPT_Stern Sternheimer 求解** +- `apply_op` 复用 `ops->hPsi(hpsi_info)`(hsolver_pw.cpp:268-274 模式)。 +- 新写 `cg_solve`(现有 `DiagoCG` 是本征 CG,不可复用);方程 `(H(k+q)−ε)|dψ⟩=−P_c dV|ψ⟩`。 +- 测试:一维谐振子/金刚石解析对照。提交。 + +**C3 — DFPT_Rho 密度响应** +- `compute_drho`:一阶密度交叉核 `Re(ψ*·dψ)` + USPP `d(⟨β|ψ⟩⟨ψ|β⟩)`(照 `elecstate_op.h` 模式新写)。 +- `mix_drho` 直接复用 `Plain_Mixing::plain_mix`(`source_base/module_mixing/plain_mixing.h:90-105`),不套 `Charge::rho`。 +- **U0 实装点**:`cal_docc` 真实交叉项(dψ 就绪后)。 +- 测试:密度求和规则/对称性。提交。 + +**C4 — DFPT_Metal(仅接口)** +- 本期不实现:`compute_drho`/occupation 响应留接口与设计说明(`is_metal_`/`dmu_` 数据已备)。 + +**C5 — DFPT_Phon 动力学矩阵** +- `assemble`:`ion_ion(q,dynmat)`(已真)+ `electron(q_idx,data,dynmat)`(已真,Ewald 复用 `force_pw.cpp:479 cal_force_ew` + `H_Ewald_pw::rgen`;相因子经 `symm.gtrans[48]`+`kgmatrix[48]`)。 +- **U0 实装点**:`dftu_onsite`/`dftu_lambda` 电子项。 +- `diagonalize` 换真实现(替换 `freq[i]=i` 伪值);`add_loto`/`check_sum_rule` 实装或明示桩。 +- 测试:金刚石 Γ 声子频率对照。提交。 + +**C6 — DFPT_Q0 介电/Born/LO-TO** +- 新增 `v_hartree_q`:`|G+q|²` Poisson 因子、跳过 ig=−q(参考 `h_hartree_pw.cpp:16-97`)。 +- XC 一阶核:库中无 v_xc 一阶 API,用 LIBXC kernel 或有限差兜底。 +- 非局域 `[r,V_U]` commutator 项记录为 U 预留。 +- 测试:金刚石介电张量/Born 电荷对照。提交。 + +**C7 — run() 接线 + ESolver/INPUT** +- 修正签名不一致:注释中 `pert_.build_dv(q,irrep,...)` 与真实 `build_dv(int q_idx,int atom_idx,int dir,DFPT_PW_Data&)`;mode basis 为空时逐 irrep 先遍历 3N 方向,irrep 收敛为代表模。 +- `esolver_dfpt_pw.cpp`:解开 `init` 注释,实参 `*this->stp.psi_cpu` + `PARAM.inp.nelec` + `PARAM.inp.ecutwfc`;`dft_plus_u` 为真时传 `&this->dftu`(否则 nullptr)。 +- INPUT 行为若变则同步 `docs/parameters.yaml` + `input-main.md`。 +- 金刚石端到端对照:声子频率 + 介电;`./build/abacus --version` 记录身份。 +- 全量构建 + 回归 + 治理。提交。 + +--- + +## B — 数据层收编 + +- `DFPT_IrrepData` 下沉为 `DFPT_PW_Data` 正式数据层(收敛台账、irrep 元数据并入),迁移现有 dfpt 测试。 +- 提交。 + +## A — irrep 分解(最后) + +- `module_symmetry/little_group.{h,cpp}`:完整不可约表示表 + 投影算子 → 真实 `get_nirr`/`get_mode_basis`(替换占位 =1/空)。 +- 测试:金刚石/闪锌矿 Γ/X/L 点 irrep 分解与理论表核对。提交。 + +--- + +## 风险与注意事项 + +- `Plus_U dftu` 是对象成员,指针永远非空 → `with_u` 由 esolver 在 `dft_plus_u` 时传非空指针决定,语义干净。 +- 无轨道文件 → `u_active()` 为 false 安全退化;测试显式覆盖该路径。 +- `dftu.cpp` 加入测试链接依赖 LCAO=OFF 配置;若 CI 在 LCAO=ON 下编译该测试需补 hamilt 依赖(记录在案)。 +- 沙箱 OpenMPI 警告(`opal_ifinit`)为环境产物,不作为失败判据。 + +## 进度 + +- [x] 计划定稿(U0/C/B/A 序列、DFT+U 依赖轨道文件的 on/off 自洽设计) +- [ ] U0 DFT+U 接口预留 +- [ ] C0 k+q 平面波基枚举 +- [ ] C1 DFPT_Pert +- [ ] C2 DFPT_Stern +- [ ] C3 DFPT_Rho +- [ ] C4 DFPT_Metal(仅接口) +- [ ] C5 DFPT_Phon +- [ ] C6 DFPT_Q0 +- [ ] C7 run() 接线 + ESolver/INPUT + 金刚石对照 +- [ ] B 数据层收编 +- [ ] A irrep 分解 diff --git a/source/source_pw/module_dfpt/README.md b/source/source_pw/module_dfpt/README.md index c3133c1195e..0bfaf97cb74 100644 --- a/source/source_pw/module_dfpt/README.md +++ b/source/source_pw/module_dfpt/README.md @@ -82,7 +82,9 @@ DFPT_PW ```cpp // In ESolver ModuleDFPT::DFPT_PW dfpt; -dfpt.init(ucell, psi, nelec, ecutwfc); +// dftu is a const Plus_U* wired by the esolver layer ONLY when dft_plus_u +// is enabled; pass nullptr otherwise (DFPT never reads PARAM itself). +dfpt.init(ucell, psi, nelec, ecutwfc, dftu); dfpt.set_qmesh(4, 4, 4); dfpt.set_conv_thr(1e-8); dfpt.run(); diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index 4c086cbf5c3..189820bbb6f 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -23,6 +23,23 @@ void DFPT_Pert::init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, } void DFPT_Pert::build_dv(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data) { + (void)q_idx; + (void)atom_idx; + (void)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::build_dv_u(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data) { + // Reserved first-order Hubbard potential dV_U (U0). + // occupation-response part: |phi(k+q)> U(diag*delta - docc) + // (needs docc from DFPT_Rho::cal_docc, SCF self-consistent) + // frozen part (C1): |dphi(k+q)/dtau> V_eff + adjoint, + // reusing Onsite_Proj_tools initialized on the DFPT k+q basis. (void)q_idx; (void)atom_idx; (void)dir; diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index fb5e27ff357..b131dbe3908 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -39,6 +39,9 @@ class DFPT_Pert { ModulePW::PW_Basis_K* pw_wfc_ = nullptr; Structure_Factor* sf_ = nullptr; + /// first-order Hubbard potential dV_U (U0 reservation, C1/C3 impl.) + void build_dv_u(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data); + void dVloc_dtau(int atom_idx, int dir, const ModuleBase::Vector3& q, std::vector>& dv); diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index 8659237d67a..f96c8fecc90 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -26,6 +26,10 @@ void DFPT_Phon::assemble(int q_idx, DFPT_PW_Data& data) { ModuleBase::Vector3 q = data.get_qvec(q_idx); ion_ion(q, dynmat); electron(q_idx, data, dynmat); + // DFT+U dynamical-matrix term (U0 reservation, implemented in C5) + if (data.with_u()) { + dftu_onsite(q_idx, data); + } data.set_dynmat(q_idx, dynmat); } @@ -63,6 +67,16 @@ void DFPT_Phon::electron(int q_idx, DFPT_PW_Data& data, ModuleBase::matrix& dyn) (void)dyn; } +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 in C5 (dftu_lambda electron term): + // 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; +} + void DFPT_Phon::ewald_sum(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn) { (void)q; (void)dyn; diff --git a/source/source_pw/module_dfpt/dfpt_phon.h b/source/source_pw/module_dfpt/dfpt_phon.h index 6e343b8cd09..7b1a54ffe26 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.h +++ b/source/source_pw/module_dfpt/dfpt_phon.h @@ -39,6 +39,9 @@ class DFPT_Phon { void electron(int q_idx, DFPT_PW_Data& data, ModuleBase::matrix& dyn); + /// DFT+U contribution to the dynamical matrix (U0 reservation, C5 impl.) + void dftu_onsite(int q_idx, DFPT_PW_Data& data); + void ewald_sum(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn); }; diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 1ebc761fa88..cf51acf043d 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -37,6 +37,7 @@ class DFPT_PW::Impl { UnitCell* ucell_ = nullptr; double nelec_ = 0.0; double ecutwfc_ = 0.0; + const Plus_U* dftu_ = nullptr; int nqx_ = 1, nqy_ = 1, nqz_ = 1; double conv_thr_ = 1e-8; @@ -50,11 +51,12 @@ DFPT_PW::~DFPT_PW() { } void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, - double nelec, double ecutwfc) { + double nelec, double ecutwfc, const Plus_U* dftu) { pimpl_->ucell_ = &ucell; pimpl_->gs_psi_ = psi; pimpl_->nelec_ = nelec; pimpl_->ecutwfc_ = ecutwfc; + pimpl_->dftu_ = dftu; std::vector mp_grid = {pimpl_->nqx_, pimpl_->nqy_, pimpl_->nqz_}; pimpl_->qlist_.generate_mesh(ucell, ucell.symm, mp_grid, true); @@ -68,7 +70,15 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, int nat = ucell.nat; pimpl_->phon_.init(ucell); - pimpl_->data_.init(&pimpl_->qlist_, nk, nbands, npw_max, nrxx, nspin, nat); + 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::run() { diff --git a/source/source_pw/module_dfpt/dfpt_pw.h b/source/source_pw/module_dfpt/dfpt_pw.h index b6ddee13eb4..7ccaf4903d8 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.h +++ b/source/source_pw/module_dfpt/dfpt_pw.h @@ -14,6 +14,8 @@ #include "source_cell/unitcell.h" #include "source_psi/psi.h" +class Plus_U; + namespace ModuleDFPT { class DFPT_PW { @@ -22,10 +24,17 @@ class DFPT_PW { ~DFPT_PW(); void init(UnitCell& ucell, const psi::Psi>& psi, - double nelec, double ecutwfc); + double nelec, double ecutwfc, const Plus_U* 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 (locale initialized, i.e. the LCAO + /// orbital files are present). + 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; diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index b0cb27c498f..ef4eb7aaac6 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -7,6 +7,7 @@ // ============================================================ #include "dfpt_pw_data.h" +#include "source_lcao/module_dftu/dftu.h" namespace ModuleDFPT { @@ -17,7 +18,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* dftu) { qlist_ = qlist; nk_ = nk; nbands_ = nbands; @@ -25,6 +26,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 +37,29 @@ void DFPT_PW_Data::clean() { is_initialized_ = false; } +bool DFPT_PW_Data::u_active() const { + // locale initialization requires the LCAO orbital files; a pure-PW run + // without them has dftu != nullptr (wired upstream) but is not usable. + return with_u() && dftu_->is_locale_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; +} + +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(); } @@ -168,6 +193,7 @@ void DFPT_PW_Data::deallocate_memory() { dynmat_.clear(); phon_freq_.clear(); born_.clear(); + docc_.clear(); residuals_.clear(); } diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index 64aaa0bd0db..a9ba03907e4 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -16,6 +16,8 @@ #include #include +class Plus_U; + namespace ModuleDFPT { class DFPT_PW_Data { @@ -24,7 +26,7 @@ class 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* dftu); void clean(); @@ -78,6 +80,23 @@ class DFPT_PW_Data { 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* + /// only then. + /// with_u(): a Plus_U provider is wired (dft_plus_u enabled upstream). + /// u_active(): the provider is additionally usable (locale initialized, + /// which requires the LCAO orbital files; a pure-PW run + /// without them must degrade to inactive safely). + bool with_u() const { return dftu_ != nullptr; } + bool u_active() const; + const Plus_U* 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; + private: ModuleCell::QList* qlist_ = nullptr; @@ -106,6 +125,10 @@ class DFPT_PW_Data { bool is_metal_ = false; double dmu_ = 0.0; + /// DFT+U reservation state (U0) + const Plus_U* dftu_ = nullptr; + std::vector>> docc_; + int max_iter_ = 100; double conv_thr_ = 1e-8; int current_iter_ = 0; diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index 89e60e3edbc..f3caf4d3063 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -34,6 +34,11 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, DFPT_PW_Da } 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. When the + // q->0 (dielectric / Born / LO-TO) response is implemented in C6, the + // [r, V_U] commutator term must be handled separately in addition to the + // occupation-matrix response (docc); this is the hardest DFT+U piece. (void)data; } diff --git a/source/source_pw/module_dfpt/dfpt_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index 4cf239adb5b..0b14a416c65 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -39,6 +39,23 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, (void)data; } +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 implementation lands in C3 once dpsi is available: + // cross term: Re(becp(k+q, dpsi) * becp(k, psi)) (response) + // frozen term: becp(k, psi) * dbecp_f(k, psi) (GS k, cal_dbecp_f) + // accumulated per (q, spin) into data.set_docc(). + if (!data.with_u()) { + return; + } + (void)psi; + (void)wg; + (void)q_idx; + (void)data; +} + void DFPT_Rho::mix_drho(int q_idx, DFPT_PW_Data& data) { (void)q_idx; (void)data; diff --git a/source/source_pw/module_dfpt/dfpt_rho.h b/source/source_pw/module_dfpt/dfpt_rho.h index 9790e583821..af05139d023 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.h +++ b/source/source_pw/module_dfpt/dfpt_rho.h @@ -30,6 +30,11 @@ class DFPT_Rho { 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); double get_residual(int q_idx, DFPT_PW_Data& data) const; diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index 5443043ded7..ac802df75b4 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -30,4 +30,7 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp + # Plus_U support shim: lets the test construct a Plus_U without pulling + # the LCAO-side DFT+U link closure (see dftu_test_support.cpp). + dftu_test_support.cpp ) diff --git a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp index 0d9a0637483..cc9d8d8c48f 100644 --- a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp @@ -169,7 +169,7 @@ class DFPT_IrrepDataTest : public testing::Test const int cal_symm_repr[2] = {0, 6}; symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, GlobalV::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); + data.init(&qlist, 1, 2, 3, 0, 1, 1, nullptr); } void clear_qlist() @@ -280,3 +280,29 @@ TEST_F(DFPT_IrrepDataTest, PerIrrepScfBookkeeping) clear_qlist(); } + +TEST_F(DFPT_IrrepDataTest, 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(); +} 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 index 25d84a302ae..4dc8f601306 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -14,6 +14,7 @@ #include "source_base/parallel_global.h" #include "source_base/global_variable.h" #include "source_estate/module_charge/charge_mixing.h" +#include "source_lcao/module_dftu/dftu.h" #include "source_pw/module_dfpt/dfpt_pw.h" pseudo::pseudo() @@ -181,7 +182,7 @@ 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; - dfpt.init(ucell, psi, 1.0, 15.0); + dfpt.init(ucell, psi, 1.0, 15.0, nullptr); dfpt.run(); // each of the 4 irreducible q points must expose 3*nat phonon modes @@ -196,7 +197,7 @@ TEST_F(DFPT_PWRunTest, DielectricAndBornAreExposed) { dfpt.set_qmesh(1, 1, 1); // Gamma-only q mesh psi::Psi> psi; - dfpt.init(ucell, psi, 1.0, 15.0); + dfpt.init(ucell, psi, 1.0, 15.0, nullptr); dfpt.run(); // design-phase stubs return default-constructed matrices @@ -207,3 +208,23 @@ TEST_F(DFPT_PWRunTest, DielectricAndBornAreExposed) EXPECT_EQ(born.nr, 0); EXPECT_EQ(born.nc, 0); } + +TEST_F(DFPT_PWRunTest, DftuReservationWithProviderButUninitializedLocale) +{ + // DFT+U reservation (U0): a non-null Plus_U is wired (dft_plus_u enabled + // upstream) but its locale is NOT initialized here because the LCAO + // orbital files are absent. with_u() must be true, u_active() must be + // false (safe pure-PW degradation), and run() must complete without + // touching any DFT+U kernel (all U hooks are no-op stubs). + Plus_U dftu; + dfpt.set_qmesh(1, 1, 1); + psi::Psi> psi; + dfpt.init(ucell, psi, 1.0, 15.0, &dftu); + EXPECT_TRUE(dfpt.get_with_u()); + EXPECT_FALSE(dfpt.get_u_active()); + dfpt.run(); + + // still produces the expected number of phonon modes per q + const int expected_modes = 3 * ucell.nat; + EXPECT_EQ(dfpt.get_phonon_freq(0).size(), expected_modes); +} diff --git a/source/source_pw/module_dfpt/test/dftu_test_support.cpp b/source/source_pw/module_dfpt/test/dftu_test_support.cpp new file mode 100644 index 00000000000..6b74b6a630e --- /dev/null +++ b/source/source_pw/module_dfpt/test/dftu_test_support.cpp @@ -0,0 +1,40 @@ +// ============================================================ +// Minimal test-support definitions for constructing Plus_U in +// the DFPT unit tests (DFT+U interface reservation, U0). +// +// In production these symbols live in module_dftu/dftu.cpp, +// which pulls a large link closure (init -> dftu_io/occup -> +// scalapack ...). The DFPT tests only need to *construct* a +// Plus_U and read the public inline accessors, so the ctor, +// dtor and static data members are replicated here instead. +// This keeps the DFPT (PW) unit tests free of the LCAO-side +// DFT+U dependency. Keep in sync with dftu.cpp. +// ============================================================ + +#include "source_lcao/module_dftu/dftu.h" + +#include + +double Plus_U::energy_u = 0.0; + +std::vector Plus_U::U = {}; + +std::vector Plus_U::U0 = {}; + +std::vector Plus_U::orbital_corr = {}; + +double Plus_U::uramping = 0.0; + +int Plus_U::omc = 0; + +int Plus_U::mixing_dftu = 0; + +int Plus_U::nspin = 0; + +bool Plus_U::Yukawa = false; + +Plus_U::Plus_U() +{} + +Plus_U::~Plus_U() +{} \ No newline at end of file From fe30cb78984181b3c866bc6b7e26f52776caf575 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 16:21:50 +0800 Subject: [PATCH 07/50] Feat: k+q plane-wave basis enumeration for DFPT (C0) DFPT_KQ_Basis enumerates the local plane-wave basis at the perturbation wavevector k+q by re-filtering the shared G grid of an initialized ground-state k-basis (PW_Basis_K) at the shifted center, avoiding new FFT grids or MP redistribution. Accessors expose the k+q basis size, the underlying G index / FFT slab index, G and G+k+q Cartesian vectors and |G+k+q|^2. A gamma_only ground-state basis is rejected because DFPT couples k and k+q symmetrically and needs the full complex G ball. Tests: 5 focused unit tests covering Gamma q=0 exact reproduction of the base ordering, the asymmetric shifted sphere, k+q translation invariance, nonzero-q agreement with a full FFT-grid brute-force reference, and the null/gamma_only guard. 7-target regression and abacus_pw_para link pass. No user-facing INPUT changes; design-phase module with README already covering the DFPT workflow (governance docs-sync warning exempt). --- source/source_pw/module_dfpt/CMakeLists.txt | 2 + .../module_dfpt/PLAN_dfpt_implementation.md | 11 +- .../source_pw/module_dfpt/dfpt_kq_basis.cpp | 99 +++++ source/source_pw/module_dfpt/dfpt_kq_basis.h | 90 +++++ .../source_pw/module_dfpt/test/CMakeLists.txt | 7 + .../module_dfpt/test/dfpt_kq_basis_test.cpp | 363 ++++++++++++++++++ 6 files changed, 570 insertions(+), 2 deletions(-) create mode 100644 source/source_pw/module_dfpt/dfpt_kq_basis.cpp create mode 100644 source/source_pw/module_dfpt/dfpt_kq_basis.h create mode 100644 source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp diff --git a/source/source_pw/module_dfpt/CMakeLists.txt b/source/source_pw/module_dfpt/CMakeLists.txt index 37141cb2a00..e5c936057fb 100644 --- a/source/source_pw/module_dfpt/CMakeLists.txt +++ b/source/source_pw/module_dfpt/CMakeLists.txt @@ -4,6 +4,7 @@ set(SOURCES dfpt_pw.cpp dfpt_pw_data.cpp dfpt_irrep_data.cpp + dfpt_kq_basis.cpp dfpt_pert.cpp dfpt_stern.cpp dfpt_rho.cpp @@ -16,6 +17,7 @@ set(HEADERS dfpt_pw.h dfpt_pw_data.h dfpt_irrep_data.h + dfpt_kq_basis.h dfpt_pert.h dfpt_stern.h dfpt_rho.h diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index d52bb91f17d..1c1c7e2216a 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -107,8 +107,15 @@ ## 进度 - [x] 计划定稿(U0/C/B/A 序列、DFT+U 依赖轨道文件的 on/off 自洽设计) -- [ ] U0 DFT+U 接口预留 -- [ ] C0 k+q 平面波基枚举 +- [x] U0 DFT+U 接口预留 `3599973fa` + - `Plus_U*` 经 `DFPT_PW::init`/`DFPT_PW_Data` 线程化(esolver 接线层决策,module_dfpt 不读全局输入) + - `with_u()`/`u_active()`(locale 未初始化即无轨道文件 → 安全退化)+ 每 q `docc_` 槽位 + - 桩:`DFPT_Rho::cal_docc`、`DFPT_Pert::build_dv_u`、`DFPT_Phon::dftu_onsite`、Q0 `[r,V_U]` 注释 + - 测试 5+3 全过;`dftu_test_support.cpp` shim(免 LCAO 侧链接闭包);6 目标回归 + `abacus_pw_para` 链接通过 +- [x] C0 k+q 平面波基枚举 + - `DFPT_KQ_Basis`:复用 GS 复杂 k 基的共享 G 网格,仅做 k+q 平移中心再过滤(`|G+k+q|^2<=gk_ecut`),无需新建 FFT 网格/重分发;前置条件 gamma_only=false + 网格截断覆盖 k+q 球(`gridecut_lat >= (sqrt(gk_ecut)+max|k|+max|q|)^2`,ecutrho>=4*ecutwfc 满足) + - `get_npwk/get_ig/get_ig2isz/get_gcar/get_gpluskq/get_gk2/get_kplusq` 访问器;gamma_only 守卫 WARNING_QUIT + - 测试 5 项全过(Γ q=0 全等复现、偏心非对称球、k+q 平移不变性、非零 q 与全网格穷举对照、null/gamma_only 拒绝);7 目标回归 - [ ] C1 DFPT_Pert - [ ] C2 DFPT_Stern - [ ] C3 DFPT_Rho 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..892d474fbff --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_kq_basis.cpp @@ -0,0 +1,99 @@ +// ============================================================ +// 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. +// ============================================================ + +#include "dfpt_kq_basis.h" +#include "source_base/global_function.h" +#include "source_basis/module_pw/pw_basis_k.h" + +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 ModuleBase::Vector3& q_cart, + int ik) +{ + pw_wfc_ = pw_wfc; + npwk_ = 0; + igl2ig_.clear(); + gk2_.clear(); + gcar_.clear(); + + if (pw_wfc_ == 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."); + } + + const ModuleBase::Vector3 k_c = pw_wfc_->kvec_c[ik]; + kplusq_c_ = k_c + q_cart; + + // Reuse the ground-state k-basis G grid (shared by all k points of the + // pool): the k+q ball is a subset of it for every ik and q (see the + // class documentation), so only the shifted-center selection is needed. + const int npw = pw_wfc_->npw; + for (int ig = 0; ig < npw; ++ig) + { + const int isz = pw_wfc_->ig2isz[ig]; + int iz = isz % pw_wfc_->nz; + const int is = isz / pw_wfc_->nz; + const int ixy = pw_wfc_->is2fftixy[is]; + int ix = ixy / pw_wfc_->fftny; + int iy = ixy % pw_wfc_->fftny; + if (ix >= int(pw_wfc_->nx / 2) + 1) + { + ix -= pw_wfc_->nx; + } + if (iy >= int(pw_wfc_->ny / 2) + 1) + { + iy -= pw_wfc_->ny; + } + if (iz >= int(pw_wfc_->nz / 2) + 1) + { + iz -= pw_wfc_->nz; + } + ModuleBase::Vector3 f(ix, iy, iz); + const ModuleBase::Vector3 gcar = f * pw_wfc_->G; + const ModuleBase::Vector3 gpluskq = gcar + kplusq_c_; + const double gk2 = gpluskq * gpluskq; + if (gk2 <= pw_wfc_->gk_ecut) + { + igl2ig_.push_back(ig); + gcar_.push_back(gcar); + gk2_.push_back(gk2); + } + } + npwk_ = static_cast(igl2ig_.size()); +} + +void DFPT_KQ_Basis::clear() +{ + pw_wfc_ = nullptr; + kplusq_c_ = ModuleBase::Vector3(); + npwk_ = 0; + igl2ig_.clear(); + gk2_.clear(); + gcar_.clear(); +} + +int DFPT_KQ_Basis::get_ig2isz(int igl) const +{ + return pw_wfc_->ig2isz[igl2ig_[igl]]; +} + +} // namespace ModuleDFPT \ No newline at end of file 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..cb5cd07b54b --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_kq_basis.h @@ -0,0 +1,90 @@ +// ============================================================ +// 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. +// ============================================================ + +#ifndef DFPT_KQ_BASIS_H +#define DFPT_KQ_BASIS_H + +#include "source_base/vector3.h" +#include + +namespace ModulePW { +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 + * re-filters the G vectors of an already-initialized ground-state k-basis + * PW_Basis_K at the shifted center k+q. Each G with |G + (k+q)|^2 <= gk_ecut + * satisfies |G| <= sqrt(gk_ecut) + |k+q|, which is within the FFT-grid ball + * (ggecut) the ground-state basis already distributed, so no G vector needed + * by k+q is missing and only the shifted-center selection is performed. + * + * Preconditions: + * - The ground-state basis must be a complex (gamma_only=false) k-basis: + * DFPT couples k and k+q symmetrically and the q-perturbation breaks the + * gamma-only half-space reduction. + * - Every G vector needed by the largest k+q ball must lie inside the FFT + * grid of the ground-state basis, i.e. the FFT-grid cutoff (gridecut_lat) + * must satisfy gridecut_lat >= (sqrt(gk_ecut) + max|k| + max|q|)^2. + * In practice ecutrho >= 4*ecutwfc covers every q inside the first + * Brillouin zone, which is the DFPT q range. + */ +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 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 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 underlying ground-state G vector + int get_ig(int igl) const { return igl2ig_[igl]; } + ///< slab index (ig2isz) of the underlying ground-state G vector + int get_ig2isz(int igl) const; + ///< 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_igl2ig() const { return igl2ig_; } + 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 igl2ig_; ///< local k+q index -> base G index + std::vector gk2_; ///< |G + (k+q)|^2 + std::vector> gcar_; ///< G in Cartesian coordinates +}; + +} // namespace ModuleDFPT + +#endif // DFPT_KQ_BASIS_H \ No newline at end of file diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index ac802df75b4..16366f088ee 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -14,6 +14,13 @@ AddTest( ../../../source_psi/psi.cpp ) +AddTest( + TARGET MODULE_DFPT_kq_basis_test + LIBS parameter base device symmetry planewave + SOURCES dfpt_kq_basis_test.cpp + ../dfpt_kq_basis.cpp +) + AddTest( TARGET MODULE_DFPT_pw_run_test LIBS parameter base device symmetry 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..06a8043bfc1 --- /dev/null +++ b/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp @@ -0,0 +1,363 @@ +#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_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 an initialized ground-state k-basis by re-filtering the + * shared G grid at the shifted center k+q. + * - Accessors get_npwk / get_ig / get_ig2isz / get_gcar / + * get_gpluskq / get_gk2 / get_kplusq. + * + * The ground-state k-basis is hand-built with public members only (no FFT + * setup needed): a complex (gamma_only=false) basis on a cubic lattice with + * an ecutwfc ball large enough to contain several G shells. Every selection + * is cross-checked against an independent brute-force count over the full + * FFT grid, and the k+q -> k+q translation invariance is verified. + */ + +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_; + const double lat0_ = 1.8897261254578281; + const double ecutwfc_ = 130.0; // gamma ball reaches the first G shell + 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; + } + + // shared field setup for a cubic complex basis; builds the FFT-grid G + // set with |G|^2 <= ggecut and fills ig2isz / is2fftixy in the same + // (stick, z) layout the real distribution code produces. + 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_.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]; + } + + // stick layout: one stick per (ix, iy) pair that has at least one + // qualifying z plane; is2fftixy[is] = iy + ix * fftny. + std::vector is2fftixy; + std::vector ig2isz; + 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_); + std::vector stick; + for (int iz0 = 0; iz0 < nz_; ++iz0) + { + const int wiz = WrapIndex(iz0, 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 * 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]; + } + } + + // 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, GammaQ0ReproducesBaseOrdering) +{ + // single Gamma k: kmaxmod = 0, so the shared grid ball equals the Gamma + // ball and the q=0 selection must reproduce the base basis verbatim. + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0)}); + ASSERT_EQ(pw_.npw, 7); + + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + ASSERT_TRUE(kq.is_valid()); + EXPECT_EQ(kq.get_npwk(), pw_.npw); + + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + EXPECT_EQ(kq.get_ig(igl), igl); // ordering preserved + EXPECT_EQ(kq.get_ig2isz(igl), pw_.ig2isz[igl]); + const ModuleBase::Vector3 gcar = kq.get_gcar(igl); + EXPECT_DOUBLE_EQ(kq.get_gk2(igl), gcar * gcar); + const ModuleBase::Vector3 gp = kq.get_gpluskq(igl); + EXPECT_DOUBLE_EQ(gp.x, gcar.x); + EXPECT_DOUBLE_EQ(gp.y, gcar.y); + EXPECT_DOUBLE_EQ(gp.z, gcar.z); + } + + // every selected vector lies inside the cutoff + 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); + } +} + +TEST_F(DFPTKQBasisTest, ShiftedCenterSelectsAsymmetricSphere) +{ + // k = (0,0,0.5): only G=(0,0,0) and G=(0,0,-1) survive the |G+k|^2 cut + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0), + ModuleBase::Vector3(0.0, 0.0, 0.5 * ModuleBase::TWO_PI / lat0_)}); + + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_, 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 * ModuleBase::TWO_PI / lat0_, 1e-12); + + // independent brute force on the full FFT grid + const std::vector> ref = ReferenceSelection(center); + ASSERT_EQ(ref.size(), 2u); // G=(0,0,0) and G=(0,0,-1) + 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); + } + + // indices must be unique (each selected vector corresponds to exactly + // one underlying G vector) + std::vector igs; + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + igs.push_back(kq.get_ig(igl)); + } + 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, 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_, k1, 0); + b.init(&pw_, ModuleBase::Vector3(0.0, 0.0, 0.0), 1); + ASSERT_EQ(a.get_npwk(), b.get_npwk()); + EXPECT_EQ(a.get_npwk(), b.get_npwk()); + for (int igl = 0; igl < a.get_npwk(); ++igl) + { + EXPECT_EQ(a.get_ig(igl), b.get_ig(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_, ModuleBase::Vector3(0.0, 0.0, 0.0) - k1, 1); + EXPECT_EQ(c.get_npwk(), 7); + 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, NonzeroQMatchesBruteForce) +{ + const ModuleBase::Vector3 k1(0.0, 0.0, 0.5 * ModuleBase::TWO_PI / lat0_); + const ModuleBase::Vector3 q(0.5 * ModuleBase::TWO_PI / lat0_, 0.0, 0.25 * ModuleBase::TWO_PI / lat0_); + BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0), k1}); + + const ModuleBase::Vector3 centers[2] = {q, k1 + q}; + for (int ik = 0; ik < 2; ++ik) + { + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_, q, ik); + const std::vector> ref = ReferenceSelection(centers[ik]); + 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); + } + // cutoff consistency: every retained plane wave is below the cut + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + EXPECT_LE(kq.get_gk2(igl), gk_ecut_ + 1e-12); + } + } +} + +TEST_F(DFPTKQBasisTest, InvalidOrGammaOnlyBaseIsRejected) +{ + // null provider: valid-but-empty basis + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(nullptr, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + EXPECT_FALSE(kq.is_valid()); + EXPECT_EQ(kq.get_npwk(), 0); + EXPECT_TRUE(kq.get_igl2ig().empty()); + + // 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_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0), + ::testing::ExitedWithCode(1), + ""); +} + +} // namespace \ No newline at end of file From dc8a716bac6e7cf13e169e6a5d9b1792b409584c Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 18:38:53 +0800 Subject: [PATCH 08/50] Feat: first-order perturbation potentials for DFPT (C1) Implement DFPT_Pert: dVloc_dtau (rho-grid coefficients with the q shift baked into magnitude and phase), the NC separable dVnl two-term identity with build_vkb/radial_vq/real_ylm, build_dv/apply_dv FFT convolution on the shared rho/wfc grid, build_efield, and the U0-reserved build_dv_u guard. dv/dpsi storage upgraded from stubs in DFPT_PW_Data. Add a serial (__MPI-off) test directory mirroring module_pw/test_serial (dfpt_planewave_serial OBJECT library) with 8 physics tests: dVloc finite difference incl. q!=0, apply_dv convolution vs analytic matrix elements, efield sawtooth closed-form FT, independent-Simpson vkb check, dVnl identity vs operator finite difference, USPP rejection, and the pure-PW DFT+U degradation. The tests caught and fixed three convention bugs: the atomic phase must be exp(i 2pi g.tau) (GS stru_fac convention, not tpiba*g.tau); the shared real-space layout is ir = (ix*ny + iy)*nz + iz (z fastest, pinned by an impulse-response probe); and rho/wfc stick tables enumerate different G balls so real_space_dv now maps through the FFT-cell (ix,iy,iz) triple instead of raw isz. Governance notes: no new GlobalV/PARAM dependencies (exception-free); the header-dependency and docs-sync warnings are covered by the forward-declared Structure_Factor and the design-phase status (no INPUT change). Verified: 8/8 serial tests, 8 ctest targets (CELL+DFPT) pass, abacus_pw_para links. --- source/source_pw/module_dfpt/CMakeLists.txt | 3 + .../module_dfpt/PLAN_dfpt_implementation.md | 9 +- source/source_pw/module_dfpt/dfpt_pert.cpp | 520 +++++++++++++- source/source_pw/module_dfpt/dfpt_pert.h | 64 +- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 92 ++- source/source_pw/module_dfpt/dfpt_pw_data.h | 31 +- .../source_pw/module_dfpt/test/CMakeLists.txt | 33 +- .../module_dfpt/test/dfpt_irrep_data_test.cpp | 8 +- .../module_dfpt/test_serial/CMakeLists.txt | 43 ++ .../test_serial/dfpt_pert_serial_test.cpp | 658 ++++++++++++++++++ 10 files changed, 1391 insertions(+), 70 deletions(-) create mode 100644 source/source_pw/module_dfpt/test_serial/CMakeLists.txt create mode 100644 source/source_pw/module_dfpt/test_serial/dfpt_pert_serial_test.cpp diff --git a/source/source_pw/module_dfpt/CMakeLists.txt b/source/source_pw/module_dfpt/CMakeLists.txt index e5c936057fb..ecced46685c 100644 --- a/source/source_pw/module_dfpt/CMakeLists.txt +++ b/source/source_pw/module_dfpt/CMakeLists.txt @@ -45,5 +45,8 @@ target_link_libraries(${MODULE_NAME} 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/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 1c1c7e2216a..60f220f3853 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -116,7 +116,14 @@ - `DFPT_KQ_Basis`:复用 GS 复杂 k 基的共享 G 网格,仅做 k+q 平移中心再过滤(`|G+k+q|^2<=gk_ecut`),无需新建 FFT 网格/重分发;前置条件 gamma_only=false + 网格截断覆盖 k+q 球(`gridecut_lat >= (sqrt(gk_ecut)+max|k|+max|q|)^2`,ecutrho>=4*ecutwfc 满足) - `get_npwk/get_ig/get_ig2isz/get_gcar/get_gpluskq/get_gk2/get_kplusq` 访问器;gamma_only 守卫 WARNING_QUIT - 测试 5 项全过(Γ q=0 全等复现、偏心非对称球、k+q 平移不变性、非零 q 与全网格穷举对照、null/gamma_only 拒绝);7 目标回归 -- [ ] C1 DFPT_Pert +- [x] C1 DFPT_Pert + - `dVloc_dtau`:rho 网格系数 `i·tpiba·(Δ+q)_dir·Vloc(|Δ+q|)·e^{i2π(Δ+q)·τ}`(Δ+q=0 分量剔除);`vloc_at_g` Coulomb 解析式(复刻 `vl_pw::vloc_coulomb`)+ numeric 径向 FT(复刻 `vloc_of_g` 含 erf 补偿) + - `dVnl_dtau`:NC 分离算符两项恒等式 `i·tpiba·(k+q+G'')_dir·(Vnl|ψ⟩ − Vnl[i·tpiba·(k+G')_dir|ψ⟩`;`build_vkb`((−i)^l·Y_lm·(4π/√Ω)∫β j_l r dr·e^{i2π·gk·τ},GS 约定对齐)+ `radial_vq`(Simpson)+ `real_ylm`(l≤2);USPP/SOC WARNING_QUIT 守卫 + - `build_dv`→`set_dv_recip_c`→recip2real→`set_dv_rc`;`apply_dv`(纯循环卷积,q 相位已并入系数)+ `build_efield`(−E·r)+ `build_dv_u`(u_active 守卫,C7 激活) + - 串行测试目录 `test_serial/`(`__MPI` 整体关闭 + `dfpt_planewave_serial` OBJECT 库,ABI 一致)8 项全过:rho_gvec≡gcar、dVloc 有限差分(含 q≠0/双方向)、apply_dv 卷积 vs 解析矩阵元、efield 斜坡闭式 FT、build_vkb 独立 Simpson+τ 纯相位、dVnl 两项恒等式 vs 算符有限差分、USPP 拒绝、with_u/u_active 安全退化 + - 测试捕获并修复 3 处约定/实现错误:① 相位幅角 `tpiba·(w·τ)` → `TWO_PI·(w·τ)`(GS `stru_fac` 的 e^{i2π(g·τ)} 约定,tau 为 lat0 单位);② 实空间布局 `ir=(ix·ny+iy)·nz+iz`(z 最快,冲击响应探针钉死;build_efield 原假设反向);③ rho/wfc 棒表枚举不同 G 球 → isz 编码不可互换,`real_space_dv` 改经 FFT 胞 (ix,iy,iz) 三元组反查 + - 已知边界(C7 处理):单 k 基时 k+q 球需 wfc G 列表含 `sqrt(gk_ecut)+|k+q|` 半径(k 网格覆盖或 inflate);并行 pool 实空间布局 + - 8 目标回归全过(CELL 4 + DFPT 4);`abacus_pw_para` 链接通过 - [ ] C2 DFPT_Stern - [ ] C3 DFPT_Rho - [ ] C4 DFPT_Metal(仅接口) diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index 189820bbb6f..051662b1a56 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -8,6 +8,17 @@ #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 + namespace ModuleDFPT { DFPT_Pert::DFPT_Pert() {} @@ -22,10 +33,128 @@ 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 phase convention (stru_fac.cpp / get_sk): 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 (Delta+q)_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) { - (void)q_idx; - (void)atom_idx; - (void)dir; + // 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). @@ -34,48 +163,377 @@ void DFPT_Pert::build_dv(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data) { } } -void DFPT_Pert::build_dv_u(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data) { - // Reserved first-order Hubbard potential dV_U (U0). - // occupation-response part: |phi(k+q)> U(diag*delta - docc) - // (needs docc from DFPT_Rho::cal_docc, SCF self-consistent) - // frozen part (C1): |dphi(k+q)/dtau> V_eff + adjoint, - // reusing Onsite_Proj_tools initialized on the DFPT k+q basis. - (void)q_idx; - (void)atom_idx; - (void)dir; - (void)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; + } + // Invert both ig -> FFT-cell mappings through the (ix,iy,iz) triple: the + // rho and wfc bases enumerate different G balls, so their isz encodings + // (stick tables) are not interchangeable - only the FFT cell position of + // a plane wave is shared between the two bases. + 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; + } + 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); + 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] * dv_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) { + // kq isz uses the wfc stick tables + const int isz = kq.get_ig2isz(igl); + 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; + const int ig_rho = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + 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_, 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 + const double gmag = gnorm / ucell_->tpiba; // |G|, 2*pi/lat0 + const double ylm = (gmag > 1.0e-10) ? real_ylm(l, mr, G * (1.0 / gmag)) : 0.0; + const double vq = radial_vq(it, ib, gnorm); + // same GS phase convention as dVloc_dtau: 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::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_, 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) { + dv_psi[iband][igl] = term_a[igl] - term_b[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 the LCAO orbital files were loaded + // (u_active()). A pure-PW run wires Plus_U non-null but the locale is not + // initialized, so no DFT+U term can be assembled yet; 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::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 b131dbe3908..9944878d50a 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 { @@ -38,18 +40,68 @@ class DFPT_Pert { ModulePW::PW_Basis* pw_rho_ = nullptr; ModulePW::PW_Basis_K* pw_wfc_ = nullptr; Structure_Factor* sf_ = nullptr; - - /// first-order Hubbard potential dV_U (U0 reservation, C1/C3 impl.) - void build_dv_u(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data); - + + /// 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) * ... + /// The sign/coefficient convention is the exact derivative of the local + /// potential with respect to the atomic displacement (see source; the unit + /// test checks it against a finite difference of the full potential incl. + /// the atomic phase). 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 + /// 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); + + /// 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. + void build_vkb(int it, int ia, + const std::vector>& gk, + std::vector>>& vkb) 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; + /// real spherical harmonic Y_{l,m}(g_hat), orthonormal convention, l<=2. + double real_ylm(int l, int m, const ModuleBase::Vector3& ghat) 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; + + /// 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_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index ef4eb7aaac6..79def11f5c5 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -78,16 +78,29 @@ 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>(); } @@ -122,17 +135,68 @@ std::vector> DFPT_PW_Data::get_drho_g(int q_idx, int spin) } 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_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::matrix& dm) { if (q_idx >= static_cast(dynmat_.size())) { dynmat_.resize(q_idx + 1); @@ -194,6 +258,10 @@ void DFPT_PW_Data::deallocate_memory() { phon_freq_.clear(); born_.clear(); docc_.clear(); + dv_r_.clear(); + dv_recip_c_.clear(); + dv_rc_.clear(); + dpsi_.clear(); residuals_.clear(); } diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index a9ba03907e4..7580dfe9f19 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -48,6 +48,18 @@ 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; + /// 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; + void set_dynmat(int q_idx, const ModuleBase::matrix& dm); ModuleBase::matrix get_dynmat(int q_idx) const; void set_phon_freq(int q_idx, const std::vector& freq); @@ -62,6 +74,16 @@ 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_; } + + /// 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_; } @@ -107,18 +129,25 @@ 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>>> dv_recip_c_; + std::vector>>> dv_rc_; + std::vector dynmat_; std::vector> phon_freq_; bool compute_q0_ = false; bool loto_ = false; + int pert_atom_ = -1; + int pert_dir_ = -1; ModuleBase::matrix dielectric_; std::vector born_; diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index 16366f088ee..4ddc47e829a 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -23,21 +23,22 @@ AddTest( AddTest( TARGET MODULE_DFPT_pw_run_test - LIBS parameter base device symmetry + LIBS parameter base device symmetry planewave SOURCES dfpt_pw_run_test.cpp - ../dfpt_pw.cpp - ../dfpt_pw_data.cpp - ../dfpt_irrep_data.cpp - ../dfpt_pert.cpp - ../dfpt_stern.cpp - ../dfpt_rho.cpp - ../dfpt_phon.cpp - ../dfpt_q0.cpp - ../dfpt_metal.cpp - ../../../source_cell/qlist.cpp - ../../../source_cell/reciprocal_grid.cpp - ../../../source_psi/psi.cpp - # Plus_U support shim: lets the test construct a Plus_U without pulling - # the LCAO-side DFT+U link closure (see dftu_test_support.cpp). - dftu_test_support.cpp + ../dfpt_pw.cpp + ../dfpt_pw_data.cpp + ../dfpt_irrep_data.cpp + ../dfpt_pert.cpp + ../dfpt_kq_basis.cpp + ../dfpt_stern.cpp + ../dfpt_rho.cpp + ../dfpt_phon.cpp + ../dfpt_q0.cpp + ../dfpt_metal.cpp + ../../../source_cell/qlist.cpp + ../../../source_cell/reciprocal_grid.cpp + ../../../source_psi/psi.cpp + # Plus_U support shim: lets the test construct a Plus_U without pulling + # the LCAO-side DFT+U link closure (see dftu_test_support.cpp). + dftu_test_support.cpp ) diff --git a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp index cc9d8d8c48f..bdf94a78c29 100644 --- a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp @@ -240,11 +240,13 @@ TEST_F(DFPT_IrrepDataTest, SetterRoundTripViaWrapper) irrep_data.set_drho_g(0, 0, 0, std::vector>(2, std::complex(1.0, 0.0))); irrep_data.set_dv_r(0, 0, 0, rho); - // the wrapper reads the same slot the setter wrote through - EXPECT_TRUE(irrep_data.get_dpsi(0, 0, 0, 0).empty()); + // the wrapper reads the same slot the setter wrote through; slots that + // are now backed by real storage return non-empty, while design-phase + // stubs still return empty. + EXPECT_FALSE(irrep_data.get_dpsi(0, 0, 0, 0).empty()); EXPECT_TRUE(irrep_data.get_drho_r(0, 0, 0).empty()); EXPECT_TRUE(irrep_data.get_drho_g(0, 0, 0).empty()); - EXPECT_TRUE(irrep_data.get_dv_r(0, 0, 0).empty()); + EXPECT_FALSE(irrep_data.get_dv_r(0, 0, 0).empty()); clear_qlist(); } 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..a7efee95326 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt @@ -0,0 +1,43 @@ +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 + # Plus_U test-support shim shared with the MPI-side dfpt tests. + ../test/dftu_test_support.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..1187998fb14 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_pert_serial_test.cpp @@ -0,0 +1,658 @@ +#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_lcao/module_dftu/dftu.h" +#include "source_psi/psi.h" + +// test-support ctor/dtor stubs (see test/dfpt_pw_run_test.cpp); the DFPT +// serial test only needs default-constructible cell/sf objects. +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() +{ +} + +/************************************************ + * 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 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; + + ModuleBase::Matrix3 latvec_; + UnitCell ucell_; + ModulePW::PW_Basis pw_rho_; + ModulePW::PW_Basis_K pw_wfc_; + Structure_Factor sf_; + ModuleDFPT::DFPT_Pert pert_; + ModuleCell::QList qlist_; + ModuleDFPT::DFPT_PW_Data data_; + + // q is generic; 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 + + void SetUp() override + { + latvec_ = ModuleBase::Matrix3(10.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 10.0); + 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 = 1000.0 * lat0_ * lat0_ * lat0_; + MakeCoulombAtom(); + + // shared-grid basis setup, mirroring setup_pwrho / setup_pwwfc + 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.push_back(q_d_); + q_cart_ = q_d_ * ucell_.G; + + data_.init(&qlist_, 1, 2, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); + } + + void TearDown() override + { + delete[] ucell_.atoms; + ucell_.atoms = nullptr; + } + + void 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; + } + + void 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; + } + + // key of an integer FFT triple (gcar * a is integral on the cubic cell) + long long FKey(int ix, int iy, int iz) const + { + return (static_cast(ix + 64) * 128 + (iy + 64)) * 128 + (iz + 64); + } + long long GKey(const ModuleBase::Vector3& g) const + { + const double a = 10.0; + return FKey(static_cast(std::llround(g.x * a)), + static_cast(std::llround(g.y * a)), + static_cast(std::llround(g.z * a))); + } + + // 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 + { + return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega / g2_bohr; + } + + // analytic dVloc/dtau_alpha coefficient at displacement vector w (1/lat0) + std::complex 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)); + } +}; + +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 + 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_, 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_, 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) +{ + 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 Plus_U (locale uninitialized) must not change the + // assembled first-order potential (U0 reservation, pure-PW degradation) + 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 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()); + } +} From 8c4c1c7d000ef188bc3e5f2999cb123de4b58ff1 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 18:45:57 +0800 Subject: [PATCH 09/50] Feat: projected CG Sternheimer solver for DFPT (C2) DFPT_Stern::solve implements the projected conjugate-gradient solution of (H(k+q) - eps_n) P_c |dpsi_n> = -P_c |dV psi_n> with P_c the projector on the complement of the occupied states at k+q (metallic branch is C4). The shifted Hamiltonian action is injected through a LinearOperator interface so the solver core stays decoupled from the ground-state operator chain; the production adapter reusing hamilt::Hamilt::ops->hPsi is wired in C7. - apply_pv: two-sweep modified Gram-Schmidt projection, alias-safe - search directions are re-projected every CG step; pAp <= 0 triggers a residual-direction restart - degenerate handling: b inside the occ subspace, b = 0, or dimension mismatch return dpsi = 0 with residual 0 - unit tests (MODULE_DFPT_stern_test, 5 cases): diagonal operator against the closed-form complement solution, dense Hermitian U D U^dagger against the spectral reference with eps inside the occupied band, orthogonality of the solution to random occupied sets, degenerate and zero right-hand sides Governance: the only findings are the two standing exemptions for this design-phase module (header value-type includes /; docs-sync with no user-visible INPUT change). Verified: MODULE_DFPT_stern_test 5/5; ctest 9/9 (CELL 4 + DFPT 5); abacus_pw_para links; governance --staged clean. --- .../module_dfpt/PLAN_dfpt_implementation.md | 13 +- source/source_pw/module_dfpt/dfpt_stern.cpp | 169 +++++-- source/source_pw/module_dfpt/dfpt_stern.h | 82 ++-- .../source_pw/module_dfpt/test/CMakeLists.txt | 7 + .../module_dfpt/test/dfpt_stern_test.cpp | 437 ++++++++++++++++++ 5 files changed, 633 insertions(+), 75 deletions(-) create mode 100644 source/source_pw/module_dfpt/test/dfpt_stern_test.cpp diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 60f220f3853..7031e5859d1 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -51,9 +51,9 @@ - 测试:dV 数值核对。提交。 **C2 — DFPT_Stern Sternheimer 求解** -- `apply_op` 复用 `ops->hPsi(hpsi_info)`(hsolver_pw.cpp:268-274 模式)。 -- 新写 `cg_solve`(现有 `DiagoCG` 是本征 CG,不可复用);方程 `(H(k+q)−ε)|dψ⟩=−P_c dV|ψ⟩`。 -- 测试:一维谐振子/金刚石解析对照。提交。 +- 移位哈密顿作用经 `LinearOperator` 抽象注入(dimension/apply):生产适配器复用 `ops->hPsi(hpsi_info)`(hsolver_pw.cpp:268-274 模式,C7 接线),单测注入解析算子。 +- 无状态 `solve(aop, occ_kq, b, max_iter, conv_thr, dpsi, residual)`:投影 CG(初始 x=0、r=P_c b,每步搜索方向重投影,收敛判据 `||P_c r||/||P_c b||`);`apply_pv` 双扫 MGS 投影(alias-safe);方程 `(H(k+q)−ε_n)P_c|dψ_n⟩=−P_c dV|ψ_n⟩`。 +- 测试:对角算子闭式解 + 稠密 Hermitian(U D U† 谱展开参考,eps 落占据带内验证投影)+ 正交性保持 + 退化 RHS。提交。 **C3 — DFPT_Rho 密度响应** - `compute_drho`:一阶密度交叉核 `Re(ψ*·dψ)` + USPP `d(⟨β|ψ⟩⟨ψ|β⟩)`(照 `elecstate_op.h` 模式新写)。 @@ -124,7 +124,12 @@ - 测试捕获并修复 3 处约定/实现错误:① 相位幅角 `tpiba·(w·τ)` → `TWO_PI·(w·τ)`(GS `stru_fac` 的 e^{i2π(g·τ)} 约定,tau 为 lat0 单位);② 实空间布局 `ir=(ix·ny+iy)·nz+iz`(z 最快,冲击响应探针钉死;build_efield 原假设反向);③ rho/wfc 棒表枚举不同 G 球 → isz 编码不可互换,`real_space_dv` 改经 FFT 胞 (ix,iy,iz) 三元组反查 - 已知边界(C7 处理):单 k 基时 k+q 球需 wfc G 列表含 `sqrt(gk_ecut)+|k+q|` 半径(k 网格覆盖或 inflate);并行 pool 实空间布局 - 8 目标回归全过(CELL 4 + DFPT 4);`abacus_pw_para` 链接通过 -- [ ] C2 DFPT_Stern +- [x] C2 DFPT_Stern + - 无状态投影 CG:`DFPT_Stern::solve`(x=0 起步,α=|r|²/(pᵀAp)、β=|r_new|²/|r_old|²,搜索方向每步 `P_c` 重投影;pAp≤0 时残差方向重启);`apply_pv` 双扫 MGS(alias-safe);收敛 `||P_c r||/||P_c b||`;末步解 hygiene 投影 + - `LinearOperator` 注入(dimension/apply):生产 hPsi 适配器留 C7;金属/dmu 分支留 C4 + - 边界行为:b 全在占据子空间 / b=0 / 维数不匹配 → dpsi=0、residual=0、返回 0 次迭代 + - 测试 5 项全过(MPI 侧 `MODULE_DFPT_stern_test`):对角算子 vs 闭式补空间解、稠密 Hermitian(Givens+相位酉 U,eps=1.7 落占据带内)vs 谱展开参考、解对随机占据集正交性 <1e-9、占据子空间退化 RHS、零 RHS + - 9 目标回归全过(CELL 4 + DFPT 5);`abacus_pw_para` 链接通过;治理仅既有两类豁免 WARNING(头文件值类型 include、设计期模块 docs-sync) - [ ] C3 DFPT_Rho - [ ] C4 DFPT_Metal(仅接口) - [ ] C5 DFPT_Phon 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 index 4ddc47e829a..065f419f493 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -21,6 +21,13 @@ AddTest( ../dfpt_kq_basis.cpp ) +AddTest( + TARGET MODULE_DFPT_stern_test + LIBS parameter base device symmetry + SOURCES dfpt_stern_test.cpp + ../dfpt_stern.cpp +) + AddTest( TARGET MODULE_DFPT_pw_run_test LIBS parameter base device symmetry planewave 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)); + } +} From e9ac116173cc784f1ebb045e649f2b404490ab28 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 14 Aug 2026 19:12:24 +0800 Subject: [PATCH 10/50] Feat: first-order density response for DFPT (C3) DFPT_Rho::compute_drho builds the q-shifted response density from the Sternheimer solutions: the periodic parts u_nk (K-basis transform) and du_nk (k+q coefficients scattered onto the rho grid through the shared FFT-cell triple, C1 pattern) multiply pointwise into A(r) = sum_{kn occ} wg u* du, real2recip gives the q-shifted coefficients A_Delta = sum_{kn} wg sum_G c*_G d_{G+Delta} indexed by the rho-grid ig, and the Delta = -q harmonic is dropped whenever -q falls on a reciprocal lattice vector (charge conservation; always at q = Gamma). The manifest real-space density 2 Re[e^{iqr} A(r)] is rebuilt from the projected coefficients so both storages agree. mix_drho applies plain mixing on the q-shifted coefficients through Base_Mixing::Plain_Mixing (zero initial input, residual ||out-in||/||out||); the heavy Charge_Mixing header dependency is replaced by a forward declaration plus a Matrix3 value member (reciprocal matrix for q_frac -> cart). - data layer: set/get_drho_r/set/get_drho_g go from stubs to real storage - guards: nspin != 1 and non-plain mixing reject with WARNING_QUIT (design phase); cal_docc stays a documented U0 reservation (needs the PW-side beta-projector adapter wired with Plus_U in the C7/U1 window) - unit tests (MODULE_DFPT_rho_serial, 5 cases): G-space coefficients against a brute-force double sum, real-space density against direct plane-wave sums, Gamma charge conservation, plain-mixing first/second step combination and residual formula - test-side findings fixed (production code verified correct): PW_Basis_K::gcar is a per-k array indexed ik*npwk_max+igl (pw_basis_k.cpp:261) and must not be read with base-ball ig; direct-sum references must pair cartesian G with cartesian r = frac . latvec - irrep wrapper test updated: drho storage slots are live (round-trip non-empty) after being design-phase stubs Governance: only the two standing exemptions for this design-phase module (value-type header includes, net dependency decreased by dropping charge_mixing.h; docs-sync with no user-visible INPUT change). Verified: MODULE_DFPT_rho_serial 5/5; ctest 10/10 (CELL 4 + DFPT 6); abacus_pw_para links; governance --staged clean apart from exemptions. --- .../module_dfpt/PLAN_dfpt_implementation.md | 15 +- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 38 +- source/source_pw/module_dfpt/dfpt_rho.cpp | 233 ++++++++++- source/source_pw/module_dfpt/dfpt_rho.h | 46 ++- .../module_dfpt/test/dfpt_irrep_data_test.cpp | 12 +- .../module_dfpt/test_serial/CMakeLists.txt | 14 + .../test_serial/dfpt_rho_serial_test.cpp | 378 ++++++++++++++++++ 7 files changed, 687 insertions(+), 49 deletions(-) create mode 100644 source/source_pw/module_dfpt/test_serial/dfpt_rho_serial_test.cpp diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 7031e5859d1..ad51a49061b 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -56,10 +56,10 @@ - 测试:对角算子闭式解 + 稠密 Hermitian(U D U† 谱展开参考,eps 落占据带内验证投影)+ 正交性保持 + 退化 RHS。提交。 **C3 — DFPT_Rho 密度响应** -- `compute_drho`:一阶密度交叉核 `Re(ψ*·dψ)` + USPP `d(⟨β|ψ⟩⟨ψ|β⟩)`(照 `elecstate_op.h` 模式新写)。 -- `mix_drho` 直接复用 `Plain_Mixing::plain_mix`(`source_base/module_mixing/plain_mixing.h:90-105`),不套 `Charge::rho`。 -- **U0 实装点**:`cal_docc` 真实交叉项(dψ 就绪后)。 -- 测试:密度求和规则/对称性。提交。 +- `compute_drho`:交叉核 `A(r)=Σ_{kn occ} wg·u*·du`(K/rho 两条 FFT 路径同网格点乘,均无 k 相位 → Bloch 相位合并为单个 e^{iq·r});`drho_g` 为 rho 网格 q 移位系数 `A_Δ=Σ wg Σ_G c*_G d_{G+Δ}`;Δ=−q 落在倒格矢时投影为零(电荷守恒,q=Γ 必触发);`drho_r=2Re[e^{iqr}A]` 由投影后系数重建。USPP 增广项与 nspin>1 留 WARNING_QUIT 守卫(设计期)。 +- `mix_drho` 直接复用 `Base_Mixing::Plain_Mixing::plain_mix`(q 移位复空间混合 + 残差 `||out−in||/||out||`,首步 in=0),不套 `Charge::rho`/`Charge_Mixing`(头依赖由 charge_mixing.h 降为前向声明 + matrix3.h 值成员)。 +- **U0 实装点**:`cal_docc` 交叉项需 k/k+q 双端 β 投影子(PW 侧 vkb 适配器),与 Plus_U 生产接线同落 C7/U1 窗口;纯 PW 路径 `u_active()` 恒 false,安全退化保持。 +- 测试:G 空间暴力双和对照 + 实空间直接求和对照 + Γ 电荷守恒(ig0 置零 + 网格和≈0)+ 混合两步组合与残差公式。提交。 **C4 — DFPT_Metal(仅接口)** - 本期不实现:`compute_drho`/occupation 响应留接口与设计说明(`is_metal_`/`dmu_` 数据已备)。 @@ -131,6 +131,13 @@ - 测试 5 项全过(MPI 侧 `MODULE_DFPT_stern_test`):对角算子 vs 闭式补空间解、稠密 Hermitian(Givens+相位酉 U,eps=1.7 落占据带内)vs 谱展开参考、解对随机占据集正交性 <1e-9、占据子空间退化 RHS、零 RHS - 9 目标回归全过(CELL 4 + DFPT 5);`abacus_pw_para` 链接通过;治理仅既有两类豁免 WARNING(头文件值类型 include、设计期模块 docs-sync) - [ ] C3 DFPT_Rho +- [x] C3 DFPT_Rho + - `compute_drho`:每 (q,k) 经 `DFPT_KQ_Basis` 重建 k+q 基,dpsi 系数经 (ix,iy,iz) 反查散布到 rho 网格(C1 模式);`u`=K 基 recip2real、`du`=rho 网格 recip2real,同网格共轭积累加 `A(r)`;real2recip → `drho_g`(q 移位系数);Δ=−q(Miller 逆解 + 舍入判定)投影零;`drho_r` 从投影后系数重建(双存储一致);占据门 `wg<1e-8` 跳过 + - `mix_drho`:`Plain_Mixing::plain_mix` 复空间混合(首步 in=0 → mixed=β·out,残差=1),混合后重建 `drho_r`;`init` 增加 `recip_matrix`(G 矩阵,q_frac→cart),非 plain 混合 WARNING_QUIT;nspin≠1 WARNING_QUIT(自旋- k 排序未钉死,C7 定) + - 数据层 `set/get_drho_r/g` 由桩转正式存储;irrep 包装测试同步翻转(round-trip 非空) + - 测试捕获并修复测试侧 2 处参考错误(生产代码无 bug):① `PW_Basis_K::gcar` 是逐 k 数组(`ik*npwk_max+igl`,pw_basis_k.cpp:261-286),按基球 ig 读是错的——参考列表改按 igl 直读;② 直接求和参考混用 cart G 与 frac r(相位差 lat0 倍)——改 `r_cart=frac·latvec` 后 `g·r_cart` + - 串行测试 5 项全过(`MODULE_DFPT_rho_serial`):G 空间 vs 暴力双和(<1e-10)、实空间 vs 直接求和(5 采样点 <1e-9)、Γ 电荷守恒(ig0 置零 + Σdrho_r/|max|/N <1e-12)、混合首步=β·out 且残差=1、第二步组合公式 + 残差 + - 10 目标回归全过(CELL 4 + DFPT 6);`abacus_pw_para` 链接通过;治理仅既有豁免 WARNING(头文件净减 charge_mixing.h) - [ ] C4 DFPT_Metal(仅接口) - [ ] C5 DFPT_Phon - [ ] C6 DFPT_Q0 diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index 79def11f5c5..e0424e0ef3a 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -111,26 +111,44 @@ psi::Psi>& DFPT_PW_Data::get_dpsi_obj(int q_idx) { } 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>(); } diff --git a/source/source_pw/module_dfpt/dfpt_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index 0b14a416c65..845aef416f2 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,34 +29,162 @@ 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) { 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; + if (mix_type != "plain") + { + ModuleBase::WARNING_QUIT("DFPT_Rho", + "only plain mixing is supported in the design phase"); + } + 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) { - (void)psi; - (void)wg; - (void)q_idx; - (void)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_; + + // rho-ig -> FFT-cell reverse map through the shared (ix,iy,iz) triple + // (the rho/wfc stick encodings are not interchangeable, C1 finding) + 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::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_, q_cart, ik); + const int npw_kq = kq.get_npwk(); + // k+q stick index -> rho-grid ig through the shared FFT cell + std::vector kq2rho(npw_kq, -1); + for (int igl = 0; igl < npw_kq; ++igl) { + const int isz = kq.get_ig2isz(igl); + 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; + kq2rho[igl] = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + } + for (int ib = 0; ib < nbands; ++ib) { + const double w = wg(ik, ib); + if (w < 1.0e-8) { + 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()); + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + a_r[ir] += w * std::conj(u_r[ir]) * d_r[ir]; + } + } + } + + // 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) + { + const int ix = (static_cast(mr[0]) % pw_rho_->nx + pw_rho_->nx) % pw_rho_->nx; + const int iy = (static_cast(mr[1]) % pw_rho_->ny + pw_rho_->ny) % pw_rho_->ny; + const int iz = (static_cast(mr[2]) % pw_rho_->nz + pw_rho_->nz) % pw_rho_->nz; + const int ig0 = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + 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 2 Re[e^{i q r} A(r)], rebuilt from the + // (conservation-projected) coefficients so both storages agree + std::vector> a_clean(pw_rho_->nrxx); + pw_rho_->recip2real(drho_g.data(), a_clean.data()); + std::vector drho_r(pw_rho_->nrxx); + 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, +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 implementation lands in C3 once dpsi is available: + // 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, cal_dbecp_f) - // accumulated per (q, spin) into data.set_docc(). + // frozen term: becp(k, psi) * dbecp_f(k, psi) (GS k) if (!data.with_u()) { return; } @@ -57,14 +195,67 @@ void DFPT_Rho::cal_docc(const psi::Psi>& psi, } 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); + 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 + const ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); + std::vector> a_clean(pw_rho_->nrxx); + pw_rho_->recip2real(mixed.data(), a_clean.data()); + std::vector drho_r(pw_rho_->nrxx); + 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]; } -} // 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 af05139d023..c099502be3a 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.h +++ b/source/source_pw/module_dfpt/dfpt_rho.h @@ -10,21 +10,48 @@ #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 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 applies plain mixing on the q-shifted coefficients: + * drho_in <- drho_in + beta (drho_out - drho_in) + * through Base_Mixing::Plain_Mixing (no Charge_Mixing / Charge dependency). + */ 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); void compute_drho(const psi::Psi>& psi, const ModuleBase::matrix& wg, int q_idx, @@ -44,11 +71,16 @@ 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; - 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/test/dfpt_irrep_data_test.cpp b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp index bdf94a78c29..c55d6d5964b 100644 --- a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp @@ -230,9 +230,9 @@ TEST_F(DFPT_IrrepDataTest, SetterRoundTripViaWrapper) ModuleDFPT::DFPT_IrrepData irrep_data(data); - // dpsi / drho / dv are still design-phase no-op storage stubs: the + // dpsi / drho / dv storage went live with C1 (dv) and C3 (drho): the // wrapper must forward the irrep-indexed calls to the per-q storage - // slot without crashing, and reads must reflect the stub (empty) + // slot and reads must return what was written std::vector> psi(3, std::complex(1.0, 2.0)); irrep_data.set_dpsi(0, 0, 0, 0, psi); std::vector rho(2, 3.0); @@ -240,12 +240,10 @@ TEST_F(DFPT_IrrepDataTest, SetterRoundTripViaWrapper) irrep_data.set_drho_g(0, 0, 0, std::vector>(2, std::complex(1.0, 0.0))); irrep_data.set_dv_r(0, 0, 0, rho); - // the wrapper reads the same slot the setter wrote through; slots that - // are now backed by real storage return non-empty, while design-phase - // stubs still return empty. + // the wrapper reads the same slot the setter wrote through EXPECT_FALSE(irrep_data.get_dpsi(0, 0, 0, 0).empty()); - EXPECT_TRUE(irrep_data.get_drho_r(0, 0, 0).empty()); - EXPECT_TRUE(irrep_data.get_drho_g(0, 0, 0).empty()); + EXPECT_FALSE(irrep_data.get_drho_r(0, 0, 0).empty()); + EXPECT_FALSE(irrep_data.get_drho_g(0, 0, 0).empty()); EXPECT_FALSE(irrep_data.get_dv_r(0, 0, 0).empty()); clear_qlist(); diff --git a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt index a7efee95326..1950ecec104 100644 --- a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt @@ -41,3 +41,17 @@ AddTest( # Plus_U test-support shim shared with the MPI-side dfpt tests. ../test/dftu_test_support.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 + # Plus_U test-support shim shared with the MPI-side dfpt tests. + ../test/dftu_test_support.cpp +) 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..bdbb140d543 --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_rho_serial_test.cpp @@ -0,0 +1,378 @@ +#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" + +/************************************************ + * 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 testing::Test +{ + protected: + const double lat0_ = 1.8897261254578281; + const double ecutwfc_ = 2.5; // Ry + const double rho_mult_ = 9.0; + + ModuleBase::Matrix3 latvec_; + ModulePW::PW_Basis pw_rho_; + ModulePW::PW_Basis_K pw_wfc_; + ModuleCell::QList qlist_; + ModuleDFPT::DFPT_PW_Data data_; + ModuleDFPT::DFPT_Rho rho_; + + 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_; + ModuleBase::Matrix3 G_; + + static const int nbands_ = 2; + + void SetUp() override + { + latvec_ = ModuleBase::Matrix3(10.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 10.0); + G_ = latvec_.Inverse().Transpose(); + + 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.push_back(q_d_); + q_cart_ = q_d_ * G_; + + data_.init(&qlist_, 1, nbands_, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + rho_.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "plain", 0.4); + } + + 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_, 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_, 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 = sum_G c*_G d_{G+Delta}, brute-forced over the two 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; + } + } + } + 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_, 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)); + const double ref = 2.0 * (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); + + psi::Psi> psi(1, nbands_, pw_wfc0.npwk_max, pw_wfc0.npwk[0], true); + ModuleDFPT::DFPT_KQ_Basis kq0; + kq0.init(&pw_wfc0, 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); +} From d1a1fc0815bee7b096fa5ca102ca68cce78e5161 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 17 Aug 2026 13:28:27 +0800 Subject: [PATCH 11/50] Feat: dynamical matrix for DFPT (C4 guard + C5) - DFPT_Metal (C4): explicit WARNING_QUIT guards on the reserved metallic branch (dfdeps/compute_dmu/compute_drho_metal); interface-only as planned - DFPT_Phon (C5): - ion_ion: Ewald force constants (G + R + self-image phase terms), the Gamma acoustic sum rule holds exactly by construction - accumulate_electron: 2n+1 complex accumulation 2 sum wg plus the same-atom anharmonic term (d2vloc_r + apply_d2vnl from DFPT_Pert); the dpsi slot is backed up/restored around apply_dv - assemble/diagonalize/add_loto/check_sum_rule: zheev with signed cm^-1 frequencies, LO-TO non-analytic term, Gamma row-sum rule - DFPT_PW_Data: dynmat stored as ComplexMatrix (complex Hermitian at generic q) - fixes found by the new serial test: the cross term dropped the imaginary part (needed for the Hermitian symmetrization at q != 0) and the test reference used the basis momentum G instead of the kernel momentum G+q - serial test MODULE_DFPT_phon_serial: 7 cases (Gamma ASR on a symmetry-broken two-atom cell, acoustic zero modes, incommensurate q vs direct dipole-Hessian sum, injected-dpsi closed-form contraction, zheev on a known matrix, isotropic LO-TO limit, Gamma sum rule) - verification: 11/11 ctest targets pass (CELL 4 + DFPT 7), abacus_pw_para links, governance shows only the two pre-existing exempt warning classes --- .../module_dfpt/PLAN_dfpt_implementation.md | 36 +- source/source_pw/module_dfpt/dfpt_metal.cpp | 19 +- source/source_pw/module_dfpt/dfpt_pert.cpp | 135 ++++ source/source_pw/module_dfpt/dfpt_pert.h | 27 +- source/source_pw/module_dfpt/dfpt_phon.cpp | 600 ++++++++++++++-- source/source_pw/module_dfpt/dfpt_phon.h | 64 +- source/source_pw/module_dfpt/dfpt_pw.cpp | 2 +- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 6 +- source/source_pw/module_dfpt/dfpt_pw_data.h | 10 +- .../module_dfpt/test_serial/CMakeLists.txt | 31 +- .../test_serial/dfpt_phon_serial_test.cpp | 647 ++++++++++++++++++ 11 files changed, 1495 insertions(+), 82 deletions(-) create mode 100644 source/source_pw/module_dfpt/test_serial/dfpt_phon_serial_test.cpp diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index ad51a49061b..34f89eb0e8c 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -65,22 +65,26 @@ - 本期不实现:`compute_drho`/occupation 响应留接口与设计说明(`is_metal_`/`dmu_` 数据已备)。 **C5 — DFPT_Phon 动力学矩阵** -- `assemble`:`ion_ion(q,dynmat)`(已真)+ `electron(q_idx,data,dynmat)`(已真,Ewald 复用 `force_pw.cpp:479 cal_force_ew` + `H_Ewald_pw::rgen`;相因子经 `symm.gtrans[48]`+`kgmatrix[48]`)。 +- `assemble`:`ion_ion(q,dynmat)` + `electron(q_idx,data,dynmat)` 真实现(Ewald α 选取复用 `force_pw.cpp:479 cal_force_ew` 惯例:1.1 起步递降、upperbound<1e-6、跳 `ig_gge0`;仓库中 `H_Ewald_pw::rgen` 已不存在,以 `cal_force_ew` 为蓝本;相因子经 `symm.gtrans[48]`+`kgmatrix[48]`)。 +- `electron` 拆两步:`accumulate_electron(q,atom,dir,psi,wg,data)`(2n+1 形式 `D_ab=(2/Nk)Σ_kn wg·⟨dψ^b|dV^a_ext|ψ⟩` 复数逐 k 累积,虚部经 k-star 配对共轭相消/assemble Hermitian 对称化吸收;run() 每方向 SCF 收敛后即时调用,dpsi 存储不加方向维度,避免 B 前翻搅数据层)+ `assemble` 合并 ion_ion + 累积电子项。 - **U0 实装点**:`dftu_onsite`/`dftu_lambda` 电子项。 -- `diagonalize` 换真实现(替换 `freq[i]=i` 伪值);`add_loto`/`check_sum_rule` 实装或明示桩。 -- 测试:金刚石 Γ 声子频率对照。提交。 +- `diagonalize` 真实现(`LapackConnector::zheev`,`ω=sign(e)√|e|` 换算 cm⁻¹,质量因子 1/√(MM′));`add_loto` 非解析项 `(4π/Ω)(q·Z*_a)(q·Z*_b)/(q·ε∞·q)/√(MM′)`;`check_sum_rule` Γ 声学 3 零模 + 列和。 +- 测试:ion_ion vs 小胞朴素双和;Γ ASR;accumulate_electron vs 注入 dpsi 闭式收缩;zheev vs 已知矩阵;loto 各向同性解析极限。提交。 **C6 — DFPT_Q0 介电/Born/LO-TO** -- 新增 `v_hartree_q`:`|G+q|²` Poisson 因子、跳过 ig=−q(参考 `h_hartree_pw.cpp:16-97`)。 -- XC 一阶核:库中无 v_xc 一阶 API,用 LIBXC kernel 或有限差兜底。 +- 新增 `v_hartree_q`:`dV_H(G)=4π|G+q|²⁻¹·drho_g`,跳过 |G+q|=0(ig=−q),约定对齐 `source_estate/module_pot/h_hartree_pw.cpp:16`;实现为 `DFPT_Rho` 成员,**同时服务 C7 全 q 点 SCF 屏蔽势**。 +- **XC 一阶核(回调注入复用 `PotXC_FDM`)**:库里已有 `elecstate::PotXC_FDM`(`source_estate/module_pot/pot_xc_fdm.cpp:39`,`δV_xc=V_xc[ρ₀+δρ]−V_xc[ρ₀]`,LCAO 侧 `veff_dh.cpp:417 cal_dH_hf_xc` 已验证同构物理)。module_dfpt 内只定义回调契约 `XC_First_Order`(抽象类,镜像 `DFPT_Stern::LinearOperator` 惯例),esolver 接线层写 `PotXC_FDM_Adapter` 注入;module_dfpt 不 include `pot_xc_fdm.h`(头依赖最小)。复 δρ 拆 Re/Im 两次调用再重组(线性叠加合法,误差 O(δρ²))。`LR::KernelXC`(LIBXC 解析核,module_lr)列为后续优化,本轮不动。 +- `pos_matrix`:不走病态位置算符,用 `[Ĥ_SCF,r]` 速度算符等价式 `⟨u_m|r|u_n⟩=(ε_m−ε_n)⁻¹⟨u_m|[V_nl,r]|u_n⟩`(m≠n);`[V_nl,r]` 复用 C1 `build_vkb` 平移基列表求导。 - 非局域 `[r,V_U]` commutator 项记录为 U 预留。 -- 测试:金刚石介电张量/Born 电荷对照。提交。 +- 测试:v_hartree_q 单 G 闭式;XC 回调 vs 解析 LDA 核 `(4/9)v_xc/ρ`;ε/Z* 金刚石对称性约束;loto 方向极限。提交。 **C7 — run() 接线 + ESolver/INPUT** -- 修正签名不一致:注释中 `pert_.build_dv(q,irrep,...)` 与真实 `build_dv(int q_idx,int atom_idx,int dir,DFPT_PW_Data&)`;mode basis 为空时逐 irrep 先遍历 3N 方向,irrep 收敛为代表模。 -- `esolver_dfpt_pw.cpp`:解开 `init` 注释,实参 `*this->stp.psi_cpu` + `PARAM.inp.nelec` + `PARAM.inp.ecutwfc`;`dft_plus_u` 为真时传 `&this->dftu`(否则 nullptr)。 -- INPUT 行为若变则同步 `docs/parameters.yaml` + `input-main.md`。 -- 金刚石端到端对照:声子频率 + 介电;`./build/abacus --version` 记录身份。 +- `DFPT_PW::init` 扩签名:`(..., pw_rho, pw_wfc, sf, wg, eig, const XC_First_Order* xc)`(规则 5:不加默认参,全调用点更新);`nrxx=pw_rho->nrxx`;`pert_/rho_/q0_/phon_` 真 init。 +- Stern 生产适配器 `HamiltShiftAdapter : DFPT_Stern::LinearOperator` 包 `p_hamilt->ops->hPsi`(`hsolver_pw.cpp:271-273` hpsi_info 模式);占据态 `occ_kq` 由 GS ψ 经 k+q 基投影(复用 `apply_pv` MGS)。 +- `run()` 实装:mode basis 为空(A 前置占位)→ 回退遍历 3N 方向;SCF 内环 `dv_sc=dv_ext+v_hartree_q+xc_->apply(drho_in)` → `stern_.solve` → `compute_drho` → `mix_drho`,残差=4*ecutwfc` 写入文档)。 +- `esolver_dfpt_pw.cpp`:解开 `init` 注释,实参 `*this->stp.psi_cpu` + `PARAM.inp.nelec` + `PARAM.inp.ecutwfc` + `this->pw_wfc`/`this->pw_rho`/`this->sf` + `dft_plus_u ? &this->dftu : nullptr`(`esolver_ks.h:63`)+ PotXC_FDM 适配器(持 GS `Charge`,复 δρ 拆 Re/Im);内层 SCF 禁调 `cal_occ_pw`(U0 治理条目);`esolver.cpp` 工厂加 `"dfpt"` 分支。 +- INPUT 行为若变则同步 `docs/parameters.yaml` + `input-main.md`;验证 `./build/abacus -h esolver_type` 与 `--check-input`。 +- 金刚石端到端对照:声学 3 零模(ASR)+ 光学支 LDA 文献区间 + 介电/Born;`./build/abacus --version` 记录身份。 - 全量构建 + 回归 + 治理。提交。 --- @@ -130,7 +134,6 @@ - 边界行为:b 全在占据子空间 / b=0 / 维数不匹配 → dpsi=0、residual=0、返回 0 次迭代 - 测试 5 项全过(MPI 侧 `MODULE_DFPT_stern_test`):对角算子 vs 闭式补空间解、稠密 Hermitian(Givens+相位酉 U,eps=1.7 落占据带内)vs 谱展开参考、解对随机占据集正交性 <1e-9、占据子空间退化 RHS、零 RHS - 9 目标回归全过(CELL 4 + DFPT 5);`abacus_pw_para` 链接通过;治理仅既有两类豁免 WARNING(头文件值类型 include、设计期模块 docs-sync) -- [ ] C3 DFPT_Rho - [x] C3 DFPT_Rho - `compute_drho`:每 (q,k) 经 `DFPT_KQ_Basis` 重建 k+q 基,dpsi 系数经 (ix,iy,iz) 反查散布到 rho 网格(C1 模式);`u`=K 基 recip2real、`du`=rho 网格 recip2real,同网格共轭积累加 `A(r)`;real2recip → `drho_g`(q 移位系数);Δ=−q(Miller 逆解 + 舍入判定)投影零;`drho_r` 从投影后系数重建(双存储一致);占据门 `wg<1e-8` 跳过 - `mix_drho`:`Plain_Mixing::plain_mix` 复空间混合(首步 in=0 → mixed=β·out,残差=1),混合后重建 `drho_r`;`init` 增加 `recip_matrix`(G 矩阵,q_frac→cart),非 plain 混合 WARNING_QUIT;nspin≠1 WARNING_QUIT(自旋- k 排序未钉死,C7 定) @@ -138,8 +141,15 @@ - 测试捕获并修复测试侧 2 处参考错误(生产代码无 bug):① `PW_Basis_K::gcar` 是逐 k 数组(`ik*npwk_max+igl`,pw_basis_k.cpp:261-286),按基球 ig 读是错的——参考列表改按 igl 直读;② 直接求和参考混用 cart G 与 frac r(相位差 lat0 倍)——改 `r_cart=frac·latvec` 后 `g·r_cart` - 串行测试 5 项全过(`MODULE_DFPT_rho_serial`):G 空间 vs 暴力双和(<1e-10)、实空间 vs 直接求和(5 采样点 <1e-9)、Γ 电荷守恒(ig0 置零 + Σdrho_r/|max|/N <1e-12)、混合首步=β·out 且残差=1、第二步组合公式 + 残差 - 10 目标回归全过(CELL 4 + DFPT 6);`abacus_pw_para` 链接通过;治理仅既有豁免 WARNING(头文件净减 charge_mixing.h) -- [ ] C4 DFPT_Metal(仅接口) -- [ ] C5 DFPT_Phon +- [x] C4 DFPT_Metal(仅接口) + - `dfdeps`/`compute_dmu`/`compute_drho_metal` 加 WARNING_QUIT 守卫("not supported in the design phase"),设计期金属分支显式拒绝而非静默错值;`sigma_`/`smearing_type_` 与数据层 `is_metal_`/`dmu_` 槽位保留 +- [x] C5 DFPT_Phon + - `ion_ion`:G 空间(Poisson 对偶恒等式,`w=G+q` 核 `w_a w_b/w²·e^{-w²/4α}`)+ 实空间(erfc Hessian 双循环,`r_c=6/√α`)+ 自项相位差;对角元 phase-free 交叉原子累积(`-√(Mb/Ma)` 系数)+ 自镜像 `(e^{i2πq·L}−1)` 项;α 选取复用 `cal_force_ew` 惯例(1.1×0.9^n,upperbound<1e-6);Γ ASR 由构造精确成立 + - `accumulate_electron`:2n+1 复数累积 `2Σwg⟨dψ^b|dV^a_ext|ψ⟩`(cross 项经 `apply_dv` 复用 C1 全部约定)+ 同原子非谐项 `Σwg⟨ψ|d²V_loc+d²V_nl|ψ⟩`(`d2vloc_r` rho 网格核 + `apply_d2vnl` 四项 β 恒等式,均已在 C1/C5 实现并测试);dpsi 槽备份/恢复(apply_dv 复用槽位) + - `diagonalize`:`LapackConnector::zheev`,`ω=sgn(e)√|e|` 换算 cm⁻¹(独立 CODATA 常数交叉验证);`add_loto` `(4πe²/Ω)(q̂Z*_a)(q̂Z*_b)/(q̂ε∞q̂)/√(MM′)`;`check_sum_rule` Γ 行和 + - 测试捕获并修复 2 处错误:① 生产 cross 项 `dot.real()` 丢虚部——q≠0 时单 k 矩阵元复数(虚部 k-star 配对相消),assemble Hermitian 对称化依赖复数项,改复数累积;② 测试期望动量缺 `+q`(用 `gpluskq` 直接当动量)——dV 系数动量是 `Δ+q`(与 C1 pert 测试 `AnalyticDVloc(gpp+q_cart)` 一致),手算数值双向定位后修正为 `w=g+q_cart`,d2 期望同步补 `wg` 占据因子 + - 串行测试 `MODULE_DFPT_phon_serial` 7 项全过:Γ ASR(双原子破对称胞)、Γ 声学 3 零模、非公度 q vs 朴素偶极 Hessian 双和、accumulate_electron vs 注入 dpsi 闭式收缩、zheev vs 已知矩阵、loto 各向同性解析极限、Γ 求和规则 + - 11 目标回归全过(CELL 4 + DFPT 7);`abacus_pw_para` 链接通过;治理仅既有两类豁免 WARNING - [ ] C6 DFPT_Q0 - [ ] C7 run() 接线 + ESolver/INPUT + 金刚石对照 - [ ] B 数据层收编 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 051662b1a56..cd31c141877 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -503,6 +503,141 @@ void DFPT_Pert::build_dv_u(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data) // (|phi(k+q)> U(diag*delta - docc) ) lands in C3 after docc. } +void DFPT_Pert::d2vloc_r(int atom_idx, int da, int db, + const ModuleBase::Vector3& q_cart, + 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); + const ModuleBase::Vector3 w = gcar + q_cart; + 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 w_da)(i w_db) = -w_da w_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_cart, + 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_, 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); + + 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 + kq_da db_ + kq_db da_ ]_mu + 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] + 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 diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index 9944878d50a..13d85b3d7e0 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -25,8 +25,12 @@ 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_; } void build_dv(int q_idx, int atom_idx, int dir, DFPT_PW_Data& data); @@ -35,6 +39,27 @@ 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 displacements on + /// the SAME atom; each derivative contributes i w_dir, w = Delta + q, so + /// the reciprocal kernel is -w_da w_db Vloc(|w|) exp(i w.tau)). 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, + const ModuleBase::Vector3& q_cart, + 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 k+q + /// basis (normal-conserving separable case). The four terms come from the + /// phase derivatives of the out (k+q+G'') and in (k+G') projectors of the + /// SAME displaced atom; they reduce to zero for a uniform translation at + /// q=0 (acoustic consistency). + void apply_d2vnl(int atom_idx, int da, int db, + const ModuleBase::Vector3& q_cart, + const psi::Psi>& psi, int k_idx, + std::vector>>& d2v_psi) const; + private: UnitCell* ucell_ = nullptr; ModulePW::PW_Basis* pw_rho_ = nullptr; diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index f96c8fecc90..6577d18c7a5 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -8,68 +8,591 @@ #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 + namespace ModuleDFPT { DFPT_Phon::DFPT_Phon() {} DFPT_Phon::~DFPT_Phon() {} -void DFPT_Phon::init(UnitCell& ucell) { +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: + // sum_{L!=0} h(L)(e^{i2pi q.L} - 1) + // = [erfc piece in the R part] + (4pi/Omega)(sq - s0 - delta_ab/3), + // where the delta/3 is the G=0 limit (w_a w_b / w^2 -> delta_ab/3). + 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): isotropic limit + for (int d = 0; d < 3; ++d) { + sq[d][d] += 1.0 / 3.0; + } + 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] - (da == db ? 1.0 / 3.0 : 0.0)); + } + } + } + + // ---------------- 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 ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); + const ModuleBase::Vector3 q_cart = q_frac * ucell_->G; + const int nk = psi.get_nk(); + const int nbands = psi.get_nbands(); + + // stash the converged dpsi of this displacement (apply_dv reuses the slot) + std::vector>>> dpsib(nk); + for (int ik = 0; ik < nk; ++ik) { + dpsib[ik].resize(nbands); + for (int ib = 0; ib < nbands; ++ib) { + dpsib[ik][ib] = data.get_dpsi(q_idx, ik, ib); + } + } + + // rho ig -> shared FFT-cell reverse map (C1/C3 pattern) + 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; + } + + 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 ---- + // complex accumulation: at a generic q the single-k matrix + // elements are complex (the imaginary parts pair-conjugate over + // the k star), and the Hermitian symmetrization in assemble + // relies on them. + 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 (wg(ik, ib) < 1.0e-8) { + 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; + } + } + dynmat_accum_(rowb, cola) += 2.0 * cross; + + // ---- same-atom anharmonic term ---- + if (iat == atom_idx && cola >= rowb) { + std::vector> dv2_r; + pert_->d2vloc_r(atom_idx, idir, dir, q_cart, 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_cart, psi, ik, chi); + // k+q scatter map for this k + DFPT_KQ_Basis kq; + kq.init(pert_->get_pw_wfc(), q_cart, ik); + const int npwk_kq = kq.get_npwk(); + for (int ib = 0; ib < nbands; ++ib) { + if (wg(ik, ib) < 1.0e-8) { + 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 isz = kq.get_ig2isz(igl); + const int iz = isz % pert_->get_pw_wfc()->nz; + const int is = isz / pert_->get_pw_wfc()->nz; + const int ixy = pert_->get_pw_wfc()->is2fftixy[is]; + const int ix = ixy / pert_->get_pw_wfc()->fftny; + const int iy = ixy % pert_->get_pw_wfc()->fftny; + const int ig_rho = + ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + 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); + } + } + dynmat_accum_(rowb, cola) += d2sum; + } + } + } + + // 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); - // DFT+U dynamical-matrix term (U0 reservation, implemented in C5) + 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); } - - data.set_dynmat(q_idx, dynmat); + // 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; + 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::ion_ion(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn) { - (void)q; - (void)dyn; -} - -void DFPT_Phon::electron(int q_idx, DFPT_PW_Data& data, ModuleBase::matrix& dyn) { - (void)q_idx; - (void)data; - (void)dyn; -} - 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 in C5 (dftu_lambda electron term): + // 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. @@ -77,9 +600,4 @@ void DFPT_Phon::dftu_onsite(int q_idx, DFPT_PW_Data& data) { (void)data; } -void DFPT_Phon::ewald_sum(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn) { - (void)q; - (void)dyn; -} - -} // 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 7b1a54ffe26..9a70619b99a 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.h +++ b/source/source_pw/module_dfpt/dfpt_phon.h @@ -11,40 +11,86 @@ #include "dfpt_pw_data.h" #include "source_cell/unitcell.h" +#include "source_psi/psi.h" + +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); - 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); - - /// DFT+U contribution to the dynamical matrix (U0 reservation, C5 impl.) + /// DFT+U contribution to the dynamical matrix (U0 reservation). void dftu_onsite(int q_idx, DFPT_PW_Data& data); - - void ewald_sum(const ModuleBase::Vector3& q, ModuleBase::matrix& dyn); + + /// 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 cf51acf043d..720ca4485bd 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -69,7 +69,7 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, int nspin = 1; int nat = ucell.nat; - pimpl_->phon_.init(ucell); + pimpl_->phon_.init(ucell, nullptr, nullptr); pimpl_->data_.init(&pimpl_->qlist_, nk, nbands, npw_max, nrxx, nspin, nat, dftu); } diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index e0424e0ef3a..8b9213dfb5c 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -215,18 +215,18 @@ std::vector> DFPT_PW_Data::get_dv_rc(int q_idx, int spin) c return std::vector>(); } -void DFPT_PW_Data::set_dynmat(int q_idx, const ModuleBase::matrix& dm) { +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) { diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index 7580dfe9f19..80d24e5b78b 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -10,6 +10,7 @@ #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" @@ -60,8 +61,11 @@ class DFPT_PW_Data { void set_dv_rc(int q_idx, int spin, const std::vector>& v); std::vector> get_dv_rc(int q_idx, int spin) const; - void set_dynmat(int q_idx, const ModuleBase::matrix& dm); - ModuleBase::matrix get_dynmat(int q_idx) 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; @@ -141,7 +145,7 @@ class DFPT_PW_Data { std::vector>>> dv_recip_c_; std::vector>>> dv_rc_; - std::vector dynmat_; + std::vector dynmat_; std::vector> phon_freq_; bool compute_q0_ = false; diff --git a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt index 1950ecec104..92bad6bf70a 100644 --- a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt @@ -46,12 +46,27 @@ 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 - # Plus_U test-support shim shared with the MPI-side dfpt tests. - ../test/dftu_test_support.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 + # Plus_U test-support shim shared with the MPI-side dfpt tests. + ../test/dftu_test_support.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 + # Plus_U test-support shim shared with the MPI-side dfpt tests. + ../test/dftu_test_support.cpp ) 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..6615bfab66a --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_phon_serial_test.cpp @@ -0,0 +1,647 @@ +#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_lcao/module_dftu/dftu.h" +#include "source_psi/psi.h" + +// test-support ctor/dtor stubs (see dfpt_pert_serial_test.cpp) +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() +{ +} + +/************************************************ + * 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 testing::Test +{ + protected: + const double lat0_ = 1.8897261254578281; + const double ecutwfc_ = 2.5; + const double rho_mult_ = 9.0; + // cubic cell in lat0 units + const double a_ = 10.0; + + ModuleBase::Matrix3 latvec_; + UnitCell ucell_; + ModulePW::PW_Basis pw_rho_; + ModulePW::PW_Basis_K pw_wfc_; + Structure_Factor sf_; + ModuleDFPT::DFPT_Pert pert_; + ModuleDFPT::DFPT_Phon phon_; + ModuleCell::QList qlist_; + ModuleDFPT::DFPT_PW_Data data_; + + 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}; + + void SetUp() override + { + 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(); + + 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.push_back(q_d_); + q_cart_ = q_d_ * ucell_.G; + + data_.init(&qlist_, 1, 2, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); + phon_.init(ucell_, &pw_rho_, &pert_); + } + + void TearDown() override + { + delete[] ucell_.atoms; + ucell_.atoms = nullptr; + delete[] ucell_.iat2it; + ucell_.iat2it = nullptr; + delete[] ucell_.iat2ia; + ucell_.iat2ia = nullptr; + } + + void 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; + } + + double VlocCoulomb(double g2_bohr) const + { + return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega / g2_bohr; + } + + // reconfigure the cell as a two-atom Z=4/Z=2 crystal breaking all symmetry + void 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; + } + + // 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); + } +}; + +// --------------------------------------------------------------------------- +// 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''. + const int npwk = pw_wfc_.npwk[0]; + psi::Psi> psi(1, 2, npwk, npwk, true); + // locate the G=0 plane wave in the k ball: getgpluskcar returns the + // cartesian k+G (in 2pi/lat0 units), so look for k+G = k_cart, i.e. G = 0 + const ModuleBase::Vector3 k_cart = k_d_ * ucell_.G; + int ig_zero = -1; + 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) + { + ig_zero = ig; + break; + } + } + ASSERT_GE(ig_zero, 0); + psi(0, 0, ig_zero) = std::complex(1.0, 0.0); + ModuleBase::matrix wg(1, 2, true); + wg(0, 0) = 2.0; + wg(0, 1) = 0.0; + + // inject a known dpsi for displacement (atom 0, dir=1) on the k+q basis + const int npwk_kq = [&]() + { + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, q_cart_, 0); + return kq.get_npwk(); + }(); + std::vector> dpsi_inj(npwk_kq, std::complex(0.0, 0.0)); + dpsi_inj[0] = std::complex(0.3, 0.1); + if (npwk_kq > 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). The RHS on the k+q basis vector igl + // carries the momentum w = Delta + q (Delta = G'' since k + q = 0 makes + // every k+q basis vector a pure reciprocal-lattice harmonic G''): + // RHS^a(G'') = i tpiba w_a Vloc(w^2) e^{i 2pi w.tau} (psi is a single + // G'=0 plane wave and the Coulomb potential has no nonlocal part). + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, q_cart_, 0); + for (int adir = 0; adir < 3; ++adir) + { + std::complex expect_cross(0.0, 0.0); + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + const ModuleBase::Vector3 g = kq.get_gpluskq(igl); // = G'' + const ModuleBase::Vector3 w = g + q_cart_; // Delta + q + const double w2 = w * w; + if (w2 < 1.0e-12) + { + continue; // Delta + q = 0 component dropped by dVloc + } + const double arg = ModuleBase::TWO_PI * (w * tau_); + const std::complex rhs = std::complex(0.0, 1.0) + * (ucell_.tpiba * w[adir]) + * VlocCoulomb(w2 * ucell_.tpiba2) + * std::complex(std::cos(arg), std::sin(arg)); + expect_cross += std::conj(dpsi_inj[igl]) * rhs; + } + std::complex expect_d2(0.0, 0.0); + if (adir >= 1) // same-atom anharmonic term, upper triangle only + { + // |u|^2 = 1 keeps only Delta=0: w = q; the production path + // accumulates the d2V expectation with the occupation weight + const double w2 = q_cart_ * q_cart_; + const double arg = ModuleBase::TWO_PI * (q_cart_ * tau_); + expect_d2 = -(ucell_.tpiba * q_cart_[1]) * (ucell_.tpiba * q_cart_[adir]) + * VlocCoulomb(w2 * ucell_.tpiba2) + * std::complex(std::cos(arg), std::sin(arg)) + * wg(0, 0); + } + const std::complex expect = 2.0 * wg(0, 0) * expect_cross + expect_d2; + // note: accumulate uses (da=adir for the column, db=1 for the row); + // d2vloc_r multiplies w_da w_db symmetrically, so the closed form + // above (dir1 x adir) matches either ordering + 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()); + } +} + +// --------------------------------------------------------------------------- +// 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 + ModuleBase::ComplexMatrix dyn0(6, 6, true); + data_.set_dynmat(0, dyn0); + 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); + // temporarily make the cell two-atom for mass lookup consistency + 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].mass = 12.0; + ucell_.atoms[1].mass = 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; + + 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_)); +} From 275dfab18d3c551d13f8113627324922a889e391 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 17 Aug 2026 14:10:01 +0800 Subject: [PATCH 12/50] Feat: q->0 response for DFPT (C6: eps, Born, v_hartree_q, XC contract) - DFPT_Rho::v_hartree_q: q-shifted first-order Hartree kernel aligned with h_hartree_pw (skips |G+q|=0), shared by the C6 response and the C7 screened potential - XC_First_Order abstract contract in module_dfpt (adapter at the esolver layer in C7, mirroring DFPT_Stern::LinearOperator injection) - DFPT_Pert::build_vkb_dk: analytic k-derivative of the beta projectors (atomic phase, radial chain rule, real-harmonic direction chain); build_vkb/build_vkb_dk made public for DFPT_Q0 reuse - DFPT_Q0::pos_matrix: velocity (commutator) form r = -i / (tpiba (eps_m - eps_n)), kinetic 2 tpiba^2 (k+G) plus the separable nonlocal derivative; degenerate pairs skipped - DFPT_Q0::compute_eps / compute_born: length-gauge denominators, m sum over all bands for Z*, conj ordering of , ionic Z on the (a,b) diagonal, phon-style dpsi slot backup/restore - serial tests: MODULE_DFPT_q0_serial (5 tests: vkb FD, kinetic analytic, nonlocal operator FD, eps two-level, born closed form) and v_hartree_q checks in MODULE_DFPT_rho_serial; 12-target regression + abacus_pw_para link pass --- .../module_dfpt/PLAN_dfpt_implementation.md | 10 +- source/source_pw/module_dfpt/dfpt_pert.cpp | 102 +++ source/source_pw/module_dfpt/dfpt_pert.h | 38 +- source/source_pw/module_dfpt/dfpt_q0.cpp | 301 +++++++- source/source_pw/module_dfpt/dfpt_q0.h | 53 +- source/source_pw/module_dfpt/dfpt_rho.cpp | 27 + source/source_pw/module_dfpt/dfpt_rho.h | 32 + .../module_dfpt/test_serial/CMakeLists.txt | 15 + .../test_serial/dfpt_q0_serial_test.cpp | 717 ++++++++++++++++++ .../test_serial/dfpt_rho_serial_test.cpp | 50 ++ 10 files changed, 1304 insertions(+), 41 deletions(-) create mode 100644 source/source_pw/module_dfpt/test_serial/dfpt_q0_serial_test.cpp diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 34f89eb0e8c..08a7c26440c 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -150,7 +150,15 @@ - 测试捕获并修复 2 处错误:① 生产 cross 项 `dot.real()` 丢虚部——q≠0 时单 k 矩阵元复数(虚部 k-star 配对相消),assemble Hermitian 对称化依赖复数项,改复数累积;② 测试期望动量缺 `+q`(用 `gpluskq` 直接当动量)——dV 系数动量是 `Δ+q`(与 C1 pert 测试 `AnalyticDVloc(gpp+q_cart)` 一致),手算数值双向定位后修正为 `w=g+q_cart`,d2 期望同步补 `wg` 占据因子 - 串行测试 `MODULE_DFPT_phon_serial` 7 项全过:Γ ASR(双原子破对称胞)、Γ 声学 3 零模、非公度 q vs 朴素偶极 Hessian 双和、accumulate_electron vs 注入 dpsi 闭式收缩、zheev vs 已知矩阵、loto 各向同性解析极限、Γ 求和规则 - 11 目标回归全过(CELL 4 + DFPT 7);`abacus_pw_para` 链接通过;治理仅既有两类豁免 WARNING -- [ ] C6 DFPT_Q0 +- [x] C6 DFPT_Q0 + - `v_hartree_q`(DFPT_Rho 成员):`dV_H(G)=e²·4π/(tpiba²·|G+q|²)·drho_g`(w=gcar+q_cart,1/lat0 单位),跳过 |G+q|=0(对齐 `h_hartree_pw.cpp` 跳 ig_gge0 惯例);同函数服务 C7 全 q 屏蔽势 + - `XC_First_Order` 抽象契约(`apply(drho_r, dvxc_r)`,module_dfpt 不 include pot_xc_fdm.h,镜像 Stern::LinearOperator 注入惯例);PotXC_FDM 适配器(复 δρ 拆 Re/Im)落 C7 esolver 层,XC 核数值对照随 C7 适配器一并测试 + - `build_vkb_dk`(C1 build_vkb 的 k 导数,转 public 供 Q0 复用):三链解析导数——原子相位 `i2πτ_dir`、径向 `vq'(g)·tpiba·ghat_dir`(radial_vq 中心差分 dg=1e-4)、实谐函数方向链 `(e_dir−ghat·ghat_dir)/|G|`(l≤2 `grad_real_ylm`);G=0 处 l≥1 行方向链奇异(测度为零,仅相位项,与 QE 同处理) + - `pos_matrix` 速度算符形式:`⟨u_m|r_d|u_n⟩=−i·⟨u_m|dH/dk_d|u_n⟩/(tpiba·(ε_m−ε_n))`([H,r]=−i·dH/dk,k 取 2π/lat0 无量纲导数与 build_vkb_dk 一致,r 出 bohr);dH/dk = 动能 `2tpiba²(k+G)_d` + 非局域 `|dvkb⟩D⟨vkb|+|vkb⟩D⟨dvkb|`(D=dion·m 选择规则,dVnl_dtau 布局);V_loc 与 k 无关;严格简并对跳过(规范依赖) + - `compute_eps`:`ε_ab=δ_ab+(8π/Ω)Σ_k wg Re[r_a r_b]/(ε_c−ε_v)/Nk`(长度规范分母,与振子强度和规则一致;绝对值标定 C7 金刚石端到端);`compute_born`:`Z*_{k,ab}=Z_k δ_ab−(4/Nk)Σ wg Re[⟨v|dV_b|m⟩⟨m|r_a|v⟩]/(ε_m−ε_v)`(m 跑全部带含占据;`⟨v|dV|m⟩=conj(dv_mv)`;经 C1 apply_dv@q=0 复用全部约定,dpsi 槽备份/恢复仿 phon 模式;离子 Z 只加 (a==b) 对角,每原子单次 set_born) + - 串行测试 `MODULE_DFPT_q0_serial` 5 项全过:build_vkb_dk vs build_vkb 中心差分(泛型 gk 列表,1e-5)、pos_matrix 动能项闭式(−i 因子/tpiba 标定/Hermitian/简对跳过)、非局域收缩 vs 算符有限差分(ψ(G=0) 列置零避开奇点)、compute_eps 二能级全系数链(复激发态敏感于 conj 位置)、compute_born vs 闭式 G 求和(含离子对角+dpsi 恢复) + - `MODULE_DFPT_rho_serial` 增 v_hartree_q 3 检查(单 G 闭式、|G+q|=0 跳过、尺寸守卫清空),6 项全过 + - 12 目标回归全过(CELL 4 + DFPT 8);`abacus_pw_para` 链接通过;治理仅既有豁免 WARNING(docs-sync) - [ ] C7 run() 接线 + ESolver/INPUT + 金刚石对照 - [ ] B 数据层收编 - [ ] A irrep 分解 diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index cd31c141877..ab0256a4d35 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -367,6 +367,108 @@ void DFPT_Pert::build_vkb(int it, int ia, } } +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_cart, const psi::Psi>& psi, int k_idx, diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index 13d85b3d7e0..f8708e7476b 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -60,6 +60,30 @@ class DFPT_Pert { 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; + private: UnitCell* ucell_ = nullptr; ModulePW::PW_Basis* pw_rho_ = nullptr; @@ -99,20 +123,14 @@ class DFPT_Pert { const psi::Psi>& psi, int k_idx, std::vector>>& dv_psi); - /// 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. - void build_vkb(int it, int ia, - const std::vector>& gk, - std::vector>>& vkb) 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; /// 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 diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index f3caf4d3063..0255adcece6 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,44 +8,301 @@ #include "dfpt_q0.h" +#include "dfpt_pert.h" +#include "source_base/constants.h" +#include "source_base/global_function.h" + +#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; } -void DFPT_Q0::compute_eps(const psi::Psi>& psi, - const ModuleBase::matrix& wg, DFPT_PW_Data& data) { - (void)psi; - (void)wg; - (void)data; +void DFPT_Q0::pos_matrix(const psi::Psi>& psi, + 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_born(const psi::Psi>& psi, DFPT_PW_Data& data) { - (void)psi; - (void)data; +void DFPT_Q0::compute_eps(const psi::Psi>& psi, + const ModuleBase::matrix& wg, + const ModuleBase::matrix& eig, DFPT_PW_Data& data) { + if (ucell_ == nullptr) { + return; + } + std::vector>>>> r_mat; + pos_matrix(psi, eig, r_mat); + const int nk = psi.get_nk(); + const int nbands = psi.get_nbands(); + ModuleBase::matrix eps(3, 3, true); + for (int a = 0; a < 3; ++a) { + for (int b = 0; b < 3; ++b) { + double chi = 0.0; + for (int ik = 0; ik < nk; ++ik) { + for (int v = 0; v < nbands; ++v) { + if (wg(ik, v) < 1.0e-8) { + continue; // empty + } + for (int c = 0; c < nbands; ++c) { + if (wg(ik, c) >= 1.0e-8) { + continue; // occupied + } + const double de = eig(ik, c) - eig(ik, v); + if (std::abs(de) < 1.0e-8) { + continue; + } + chi += wg(ik, v) + * (r_mat[ik][v][c][a] * r_mat[ik][c][v][b]).real() / de; + } + } + } + eps(a, b) = ((a == b) ? 1.0 : 0.0) + + 8.0 * ModuleBase::PI / ucell_->omega * chi / nk; + } + } + 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 || pert_ == nullptr) { + return; + } + std::vector>>>> r_mat; + pos_matrix(psi, eig, r_mat); + const int nk = psi.get_nk(); + const int nbands = psi.get_nbands(); + const int nat = ucell_->nat; + + // stash the q=0 dpsi slots (apply_dv reuses them, phon backup pattern) + std::vector>>> dpsib(nk); + for (int ik = 0; ik < nk; ++ik) { + dpsib[ik].resize(nbands); + for (int ib = 0; ib < nbands; ++ib) { + dpsib[ik][ib] = data.get_dpsi(0, ik, ib); + } + } + + for (int iat = 0; iat < nat; ++iat) { + ModuleBase::matrix zstar(3, 3, true); + for (int idir = 0; idir < 3; ++idir) { + // dV matrix elements at q = 0 through the C1 path; apply_dv + // delivers dV|u_v> on the k+q = k basis for every k. + pert_->build_dv(0, iat, idir, data); + std::vector acc(3, 0.0); + for (int ik = 0; ik < nk; ++ik) { + pert_->apply_dv(0, ik, psi, data); + for (int v = 0; v < nbands; ++v) { + if (wg(ik, v) < 1.0e-8) { + continue; // empty + } + const std::vector> rhs = + data.get_dpsi(0, ik, v); + if (rhs.empty()) { + continue; + } + for (int m = 0; m < nbands; ++m) { + const double de = eig(ik, m) - eig(ik, v); + if (std::abs(de) < 1.0e-8) { + continue; // m == v or degenerate partner + } + std::complex dv_mv(0.0, 0.0); + for (size_t ig = 0; ig < rhs.size(); ++ig) { + dv_mv += std::conj(psi(ik, m, ig)) * rhs[ig]; + } + // = conj(dv_mv), multiplied from the + // right by (Gonze-Lee ordering) + for (int a = 0; a < 3; ++a) { + acc[a] += wg(ik, v) + * (std::conj(dv_mv) * r_mat[ik][m][v][a]).real() / de; + } + } + } + } + for (int a = 0; a < 3; ++a) { + zstar(a, idir) = -4.0 / nk * acc[a]; + } + } + // 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); + } + + // restore the stashed q=0 dpsi + for (int ik = 0; ik < nk; ++ik) { + for (int ib = 0; ib < nbands; ++ib) { + if (!dpsib[ik][ib].empty()) { + data.set_dpsi(0, ik, ib, dpsib[ik][ib]); + } + } + } } 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. When the - // q->0 (dielectric / Born / LO-TO) response is implemented in C6, the - // [r, V_U] commutator term must be handled separately in addition to the - // occupation-matrix response (docc); this is the hardest DFT+U piece. + // 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; } -void DFPT_Q0::pos_matrix(const psi::Psi>& psi, - std::vector>>>& r_mat) { - (void)psi; - (void)r_mat; -} - -} // 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..ca25633e22f 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.h +++ b/source/source_pw/module_dfpt/dfpt_q0.h @@ -17,30 +17,67 @@ 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) / Nk + * Born charges from dP/dtau (King-Smith/Resta Berry phases; the m sum runs + * over ALL bands, occupied and empty, m != v): + * Z*_k,ab = Z_k delta_ab - (4/Nk) sum_{k,v occ,m!=v} wg + * * Re[] / (eps_m - eps_v) + * The bare displacement potential dV/dtau comes from DFPT_Pert (C1) at + * q = 0; 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); + ModulePW::PW_Basis_K* pw_wfc, DFPT_Pert* pert); - void compute_eps(const psi::Psi>& psi, - const ModuleBase::matrix& wg, DFPT_PW_Data& data); + void compute_eps(const psi::Psi>& psi, + const ModuleBase::matrix& wg, + const ModuleBase::matrix& eig, DFPT_PW_Data& data); - void compute_born(const psi::Psi>& psi, 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). + void pos_matrix(const psi::Psi>& psi, + const ModuleBase::matrix& eig, + std::vector>>>>& r_mat); + 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 845aef416f2..73cd6b818aa 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -258,4 +258,31 @@ double DFPT_Rho::get_residual(int q_idx, DFPT_PW_Data& data) const { 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 diff --git a/source/source_pw/module_dfpt/dfpt_rho.h b/source/source_pw/module_dfpt/dfpt_rho.h index c099502be3a..656b305fa37 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.h +++ b/source/source_pw/module_dfpt/dfpt_rho.h @@ -24,6 +24,28 @@ 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 fully overwritten. + virtual void apply(const std::vector>& drho_r, + std::vector>& dvxc_r) const = 0; +}; + /** * @brief First-order density response (C3). * @@ -64,6 +86,16 @@ class DFPT_Rho { void mix_drho(int q_idx, DFPT_PW_Data& data); + /// 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; private: diff --git a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt index 92bad6bf70a..18d0bed4981 100644 --- a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt @@ -70,3 +70,18 @@ AddTest( # Plus_U test-support shim shared with the MPI-side dfpt tests. ../test/dftu_test_support.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 + # Plus_U test-support shim shared with the MPI-side dfpt tests. + ../test/dftu_test_support.cpp +) 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..59972aa30fb --- /dev/null +++ b/source/source_pw/module_dfpt/test_serial/dfpt_q0_serial_test.cpp @@ -0,0 +1,717 @@ +#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_lcao/module_dftu/dftu.h" +#include "source_psi/psi.h" + +// test-support ctor/dtor stubs (see dfpt_pert_serial_test.cpp) +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() +{ +} + +/************************************************ + * 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 full prefactor chain (8 pi / Omega, wg, + * 1/(eps_c - eps_v)) on a two-level toy system with a complex excited + * state. + * - 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 testing::Test +{ + protected: + const double lat0_ = 1.8897261254578281; + const double ecutwfc_ = 2.5; + 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_; + Structure_Factor sf_; + ModuleDFPT::DFPT_Pert pert_; + ModuleDFPT::DFPT_Q0 q0_; + ModuleCell::QList qlist_; + ModuleDFPT::DFPT_PW_Data data_; + + const ModuleBase::Vector3 k_d_{0.0, 0.0, 0.0}; // Gamma only + const ModuleBase::Vector3 tau_{1.1, 2.3, 0.7}; // lat0 units + 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 + { + 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(); + + 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.push_back(ModuleBase::Vector3(0.0, 0.0, 0.0)); + + data_.init(&qlist_, 1, 4, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); + q0_.init(ucell_, &pw_rho_, &pw_wfc_, &pert_); + } + + void TearDown() override + { + delete[] ucell_.atoms; + ucell_.atoms = nullptr; + delete[] ucell_.iat2it; + ucell_.iat2it = nullptr; + delete[] ucell_.iat2ia; + ucell_.iat2ia = nullptr; + } + + void 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 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.0) / (2.0 * 0.3 * 0.3)); + p.betar(1, i) = std::exp(-std::pow(r - 1.2, 2.0) / (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; + } + + // 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; + } + + double VlocCoulomb(double g2_bohr) const + { + return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega + / g2_bohr; + } + + // analytic dVloc/dtau_dir coefficient at displacement vector w (1/lat0) + std::complex 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)); + } +}; + +// --------------------------------------------------------------------------- +// 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 on a two-level system with a complex excited state +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, ComputeEpsTwoLevelAnalytic) +{ + 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_); + // v = sqrt(0.6)|G0> + sqrt(0.4)|Gx> (eps = 0.4 e1) + // c = sqrt(0.2)|G0> - sqrt(0.3)|Gx> + i sqrt(0.5)|Gy> (eps = e1) + // (the relative i phase keeps the reference sensitive to conj placement) + 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); + psi(0, 1, igx) = -std::sqrt(0.3); + psi(0, 1, igy) = std::complex(0.0, std::sqrt(0.5)); + + 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; + + q0_.compute_eps(psi, wg, eig, data_); + const ModuleBase::matrix eps = data_.get_dielectric(); + + // closed-form velocity elements between the two states (kinetic operator + // diagonal in G: G0 pairs G0 with G = 0, the Gy component of c pairs + // with the vanishing Gy component of v) + std::complex p01[3]; + for (int d = 0; d < 3; ++d) + { + p01[d] = 2.0 * ucell_.tpiba2 + * (std::sqrt(0.6) * std::sqrt(0.2) * 0.0 + + std::sqrt(0.4) * (-std::sqrt(0.3)) * gx_[d]); + } + const double de = eig(0, 1) - eig(0, 0); + for (int a = 0; a < 3; ++a) + { + for (int b = 0; b < 3; ++b) + { + const std::complex r_vc + = std::complex(0.0, -1.0) * p01[a] / (ucell_.tpiba * (eig(0, 0) - eig(0, 1))); + const std::complex r_cv + = std::complex(0.0, -1.0) * std::conj(p01[b]) + / (ucell_.tpiba * (eig(0, 1) - eig(0, 0))); + const double expect = ((a == b) ? 1.0 : 0.0) + + 8.0 * ModuleBase::PI / ucell_.omega * wg(0, 0) + * (r_vc * r_cv).real() / de; + EXPECT_NEAR(eps(a, b), expect, 1.0e-10) << "a=" << a << " b=" << b; + } + } +} + +// --------------------------------------------------------------------------- +// compute_born against the closed-form q = 0 sums (Coulomb local dV) +// --------------------------------------------------------------------------- + +TEST_F(DFPTQ0SerialTest, ComputeBornAnalyticCoulomb) +{ + 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_); + 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); + psi(0, 1, igx) = -std::sqrt(0.3); + psi(0, 1, igy) = std::complex(0.0, std::sqrt(0.5)); + + 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 must restore it + const std::vector> sentinel(npwk, std::complex(0.5, -0.25)); + data_.set_dpsi(0, 0, 0, sentinel); + + q0_.compute_born(psi, wg, eig, data_); + const ModuleBase::matrix zstar = data_.get_born(0); + + // closed-form dV matrix elements: supp(v) = {G0, Gx}, supp(m) = {G0, Gx, Gy} + const std::complex cv[3] = {psi(0, 0, ig0), psi(0, 0, igx), std::complex(0.0, 0.0)}; + const ModuleBase::Vector3 gv[3] = {ModuleBase::Vector3(0.0, 0.0, 0.0), gx_, + gy_}; + const std::complex cm[3] = {psi(0, 1, ig0), psi(0, 1, igx), psi(0, 1, igy)}; + const double de = eig(0, 1) - eig(0, 0); + for (int idir = 0; idir < 3; ++idir) + { + // dv_m0 = = sum_{G'' in supp(m)} cc_m(G'') + // sum_{G' in supp(v)} c_v(G') AnalyticDVloc(G'' - G') + std::complex dv10(0.0, 0.0); + for (int im = 0; im < 3; ++im) + { + if (cm[im] == std::complex(0.0, 0.0)) + { + continue; + } + for (int iv = 0; iv < 2; ++iv) + { + dv10 += std::conj(cm[im]) * cv[iv] * AnalyticDVloc(idir, gv[im] - gv[iv]); + } + } + for (int a = 0; a < 3; ++a) + { + // p_01^a = 2 tpiba^2 , kinetic operator diagonal in G: + // only shared components pair (G0 = 0 drops out, v has no Gy) + std::complex p01_a(0.0, 0.0); + for (int g = 0; g < 3; ++g) + { + p01_a += 2.0 * ucell_.tpiba2 * std::conj(cv[g]) * gv[g][a] * cm[g]; + } + const std::complex r_10 + = std::complex(0.0, -1.0) * std::conj(p01_a) + / (ucell_.tpiba * (eig(0, 1) - eig(0, 0))); + // ionic Z sits on the (a == idir) diagonal only + const double zion = (a == idir) ? ucell_.atoms[0].ncpp.zv : 0.0; + const double expect + = zion - 4.0 * wg(0, 0) * (std::conj(dv10) * r_10).real() / de; + EXPECT_NEAR(zstar(a, idir), expect, 1.0e-9) << "a=" << a << " idir=" << idir; + } + } + + // the q = 0 dpsi slot is restored + 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()); + } +} 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 index bdbb140d543..92d8c01096d 100644 --- 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 @@ -376,3 +376,53 @@ TEST_F(DFPTRhoSerialTest, MixDrhoSecondStepCombinesCorrectly) } 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()); +} From 78419b1962d661f1f656681f4c05aa89494d35c4 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 17 Aug 2026 18:27:04 +0800 Subject: [PATCH 13/50] Feat: wire DFPT driver and esolver factory (C7) Module layer (C7a): - DFPT_PW::init new signature (ucell, psi, bases, sf, veff_r, wg, eig, xc contract, nelec, ecutwfc, dftu); Impl holds GS data + hamilt_ - DFPT_HamiltShift: self-assembled H(k+q) Sternheimer operator (kinetic diagonal + veff FFT convolution + cached k+q vkb nonlocal), replacing the GS HamiltPW chain which is ik-index-bound - DFPT_Pert::apply_vr public (screened response potential on all bands, FFT-cell triple core shared with real_space_dv) - DFPT_Rho::reset_mixing per displacement; build_occ_kq folds k+q onto the GS k list; solve_displacement full SCF inner loop (v_hartree_q + xc_->apply -> RHS -> Sternheimer -> drho -> mix) - run(): q=0 response + per-irrep displacement loop + assemble / diagonalize / add_loto; null-bases skeleton fallback kept Esolver layer (C7b): - ESolver_DFPT_PW: static config + inp-captured scalars in before_all_runners (rule 1: no global record re-read), run_gs -> init_dfpt wiring after SCF convergence (veff_smooth row, wg, ekb, psi, XC_First_Order_FDM adapter splitting Re/Im through PotXC_FDM) - esolver.cpp factory 'dfpt' branch; read_inp_sys esolver_types + docs/parameters.yaml + input-main.md updated Verified: ctest 12/12 (CELL 4 + DFPT 8); abacus_pw_para links; -h esolver_type shows dfpt; --version v3.11.0-beta8. Governance: 1 allowed exception (determine_type factory PARAM read, existing pattern) + known header/docs WARNINGs. --- docs/advanced/input_files/input-main.md | 1 + docs/parameters.yaml | 1 + source/source_esolver/esolver.cpp | 9 + source/source_esolver/esolver_dfpt_pw.cpp | 143 +++++++- source/source_esolver/esolver_dfpt_pw.h | 24 ++ .../module_parameter/read_inp_sys.cpp | 7 +- source/source_pw/module_dfpt/CMakeLists.txt | 2 + .../module_dfpt/PLAN_dfpt_implementation.md | 17 +- .../module_dfpt/dfpt_hamilt_shift.cpp | 177 ++++++++++ .../source_pw/module_dfpt/dfpt_hamilt_shift.h | 97 ++++++ source/source_pw/module_dfpt/dfpt_pert.cpp | 27 +- source/source_pw/module_dfpt/dfpt_pert.h | 20 ++ source/source_pw/module_dfpt/dfpt_pw.cpp | 305 ++++++++++++++++-- source/source_pw/module_dfpt/dfpt_pw.h | 62 +++- source/source_pw/module_dfpt/dfpt_rho.cpp | 12 + source/source_pw/module_dfpt/dfpt_rho.h | 6 + .../source_pw/module_dfpt/test/CMakeLists.txt | 1 + .../module_dfpt/test/dfpt_pw_run_test.cpp | 10 +- 18 files changed, 860 insertions(+), 61 deletions(-) create mode 100644 source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp create mode 100644 source/source_pw/module_dfpt/dfpt_hamilt_shift.h diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 502c22442da..775af584f41 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -632,6 +632,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 diff --git a/docs/parameters.yaml b/docs/parameters.yaml index bef9069df66..1ef9c581911 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: "" diff --git a/source/source_esolver/esolver.cpp b/source/source_esolver/esolver.cpp index 135f6991d05..c5d7c6eac89 100644 --- a/source/source_esolver/esolver.cpp +++ b/source/source_esolver/esolver.cpp @@ -1,6 +1,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_io/module_parameter/parameter.h" @@ -49,6 +50,10 @@ std::string determine_type() { esolver_type = "ksdft_pw"; } + else if (PARAM.inp.esolver_type == "dfpt") + { + esolver_type = "dfpt_pw"; + } } else if (PARAM.inp.basis_type == "lcao_in_pw") { @@ -156,6 +161,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_esolver/esolver_dfpt_pw.cpp b/source/source_esolver/esolver_dfpt_pw.cpp index 0f80aebd222..f7fea6eb9c0 100644 --- a/source/source_esolver/esolver_dfpt_pw.cpp +++ b/source/source_esolver/esolver_dfpt_pw.cpp @@ -8,7 +8,84 @@ #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/tool_quit.h" +#include "source_io/module_parameter/parameter.h" +#include "source_pw/module_dfpt/dfpt_pw.h" +#include "source_pw/module_dfpt/dfpt_rho.h" + +#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; + // real part: V_xc[rho0 + Re drho] - V_xc[rho0] + for (int ir = 0; ir < nrxx; ++ir) + { + chg1_->rho[0][ir] = 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] = veff_1_(0, ir); + } + // imaginary part: V_xc[rho0 + Im drho] - V_xc[rho0] + for (int ir = 0; ir < nrxx; ++ir) + { + chg1_->rho[0][ir] = 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) * veff_1_(0, ir); + } + } + + private: + elecstate::PotXC_FDM* fdm_ = nullptr; + Charge* chg1_ = nullptr; + mutable ModuleBase::matrix veff_1_; + const UnitCell* ucell_ = nullptr; +}; + +} // namespace namespace ModuleESolver { @@ -18,7 +95,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 +107,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 +123,21 @@ 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; the ground-state data wiring happens in + // init_dfpt after the SCF has converged + dfpt_ = new ModuleDFPT::DFPT_PW(); + dfpt_->set_parameters("dfpt.in"); + dfpt_->set_qmesh(1, 1, 1); + dfpt_->set_conv_thr(1e-8); + dfpt_->set_max_iter(100); } void ESolver_DFPT_PW::runner(BaseCell& basecell, const int istep) @@ -57,6 +155,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,17 +187,39 @@ void ESolver_DFPT_PW::init_dfpt(UnitCell& ucell) { ModuleBase::TITLE("ESolver_DFPT_PW", "init_dfpt"); - dfpt_ = new ModuleDFPT::DFPT_PW(); - - // dfpt_->init(ucell, *this->stp.psi_cpu, nelec, ecutwfc, - // (dft_plus_u_enabled ? &this->dftu : nullptr)); + 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_->set_parameters("dfpt.in"); + // 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_qmesh(1, 1, 1); + // 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_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) 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_io/module_parameter/read_inp_sys.cpp b/source/source_io/module_parameter/read_inp_sys.cpp index 74b21ae33d3..39e303747f9 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_pw/module_dfpt/CMakeLists.txt b/source/source_pw/module_dfpt/CMakeLists.txt index ecced46685c..6ff9239d2b2 100644 --- a/source/source_pw/module_dfpt/CMakeLists.txt +++ b/source/source_pw/module_dfpt/CMakeLists.txt @@ -11,6 +11,7 @@ set(SOURCES dfpt_phon.cpp dfpt_q0.cpp dfpt_metal.cpp + dfpt_hamilt_shift.cpp ) set(HEADERS @@ -24,6 +25,7 @@ set(HEADERS dfpt_phon.h dfpt_q0.h dfpt_metal.h + dfpt_hamilt_shift.h ) add_library(${MODULE_NAME} ${SOURCES} ${HEADERS}) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 08a7c26440c..a512f0efc28 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -159,6 +159,21 @@ - 串行测试 `MODULE_DFPT_q0_serial` 5 项全过:build_vkb_dk vs build_vkb 中心差分(泛型 gk 列表,1e-5)、pos_matrix 动能项闭式(−i 因子/tpiba 标定/Hermitian/简对跳过)、非局域收缩 vs 算符有限差分(ψ(G=0) 列置零避开奇点)、compute_eps 二能级全系数链(复激发态敏感于 conj 位置)、compute_born vs 闭式 G 求和(含离子对角+dpsi 恢复) - `MODULE_DFPT_rho_serial` 增 v_hartree_q 3 检查(单 G 闭式、|G+q|=0 跳过、尺寸守卫清空),6 项全过 - 12 目标回归全过(CELL 4 + DFPT 8);`abacus_pw_para` 链接通过;治理仅既有豁免 WARNING(docs-sync) -- [ ] C7 run() 接线 + ESolver/INPUT + 金刚石对照 +- [x] C7 run() 接线 + ESolver/INPUT(模块层 + esolver 工厂接线完成;金刚石端到端数值对照随 B 前置验证补做) + - 模块层(C7a): + - `DFPT_PW::init` 新签名 `(ucell, psi, pw_rho, pw_wfc, sf, veff_r, wg, eig, xc, nelec, ecutwfc, dftu)`(规则 5:不加默认参,全调用点更新,含 pw_run_test 骨架模式传空基);Impl 持 GS 基/veff/wg/eig + `DFPT_HamiltShift* hamilt_` + `occ_kq_` 缓存 + - `DFPT_HamiltShift : DFPT_Stern::LinearOperator`(新文件 `dfpt_hamilt_shift.{h,cpp}`):H(k+q) 不复用 GS HamiltPW 链(ik 索引绑定 gk2/vkb,不可平移)→ 自组装三部分——动能 `tpiba²·kq.get_gk2(igl)` 对角 + veff_r FFT 卷积(kq2rho_ 经 FFT-cell triple 映射,C1 惯例)+ 缓存 k+q vkb 的分离非局域(dion m 选择规则同 dVnl_dtau 布局);`set_context(q_idx,k_idx)` 缓存投影 / `set_shift(eps)` 每 solve 更新对角 + - `DFPT_Pert::apply_vr`(public):屏蔽响应势作用全带(v_sc_r 与 dv_rc 同约定:q 移位复周期振幅);`real_space_dv` 重构为委托私有 `apply_vr_core`(FFT-cell triple 散射/收集核心共用);`build_vkb/build_vkb_dk` 保持 public(Q0 复用) + - `DFPT_Rho::reset_mixing(q_idx)`:清 drho_in_/residual_,每位移重开 SCF + - `build_occ_kq(q_idx)`:k+q 折叠匹配 GS k 列表(`kq ≡ k' (mod G)` 容差 1e-8;不匹配 WARNING_QUIT,需 Monkhorst 网格);占据态经共享 FFT-cell triple 从 ikq 的 G 球映射到 k+q 列表 + - `solve_displacement(q_idx,iat,idir)` 完整位移级 SCF 内环:`v_hartree_q(drho_g) + xc_->apply(drho_r)` 组 v_sc_r → `apply_dv + apply_vr` 组 RHS → `set_shift + stern_.solve` 每占据带 → `compute_drho + mix_drho` 残差收敛判据 + - `run()`:q=0 时 q0 响应(eps/Born/loto);每 irrep 位移循环 + `accumulate_electron`;`assemble + diagonalize + add_loto`(loto 方向默认 (1,1,1)/√3,一般方向随 A 阶段 irrep 机制);null 基保持骨架首迭代收敛退化(测试兼容) + - esolver 层(C7b): + - `esolver_dfpt_pw.{h,cpp}` 重写:`before_all_runners` 只做静态配置 + 从 `inp` 捕获 nspin/nelec/ecutwfc/dft_plus_u(规则 1:显式传递,init_dfpt 不读全局记录);`runner` 先 `run_gs`(复用 `ESolver_KS_PW::runner`)→ `init_dfpt` 真接线(GS 收敛后 veff/charge/psi 才存在)→ `dfpt_->run()` + - `init_dfpt` 实参:`*this->stp.psi_cpu`、`this->pw_rho/pw_wfc`、`&this->sf`、`get_veff_smooth()` 行 0 展开(`update_from_charge` 每迭代调 `interpolate_vrs`,收敛后即当前值)、`pelec->wg/ekb`、`XC_First_Order_FDM` 适配器(Re/Im 拆分过 `PotXC_FDM` 有限差分核,线性重组精确到 O(|δρ|²);持 GS Charge + scratch Charge)、`dft_plus_u ? &this->dftu : nullptr`;守卫:nspin≠1 / charge 不在 rho 网格(USPP)/ veff_smooth 网格不匹配 → WARNING_QUIT + - `esolver.cpp` 工厂:`determine_type` pw 分支加 `"dfpt"→"dfpt_pw"` + `init_esolver` 分支(治理豁免:determine_type 既有 PARAM 读取惯例,1 行) + - `read_inp_sys.cpp`:esolver_types 合法值加 `"dfpt"` + 注释/description 更新;`docs/parameters.yaml` + `docs/advanced/input_files/input-main.md` 同步 + - 验证:`cmake --build` esolver/abacus_pw_para/12 测试目标全绿;ctest 12/12(CELL 4 + DFPT 8);`abacus_pw_para -h esolver_type` 显示 dfpt 条目;`--version` v3.11.0-beta8;治理仅 determine_type 工厂 1 处豁免 ERROR + 既有 header/docs WARNING + - 待办(随 B/前置验证):金刚石端到端声子/ε∞/Z* 对照、`--check-input` 从有效算例目录验证 - [ ] B 数据层收编 - [ ] A irrep 分解 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..bbf1346abf7 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp @@ -0,0 +1,177 @@ +// ============================================================ +// 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_, q_cart, k_idx); + const int npw = kq_.get_npwk(); + + // rho ig -> shared FFT-cell reverse map, then k+q -> rho through the + // (ix,iy,iz) triple (the stick encodings of the two bases differ) + 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; + } + kq2rho_.assign(npw, -1); + for (int igl = 0; igl < npw; ++igl) { + const int isz = kq_.get_ig2isz(igl); + 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; + kq2rho_[igl] = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + } + + // 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]; + } + } + } +} + +} // 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..b499482be21 --- /dev/null +++ b/source/source_pw/module_dfpt/dfpt_hamilt_shift.h @@ -0,0 +1,97 @@ +// ============================================================ +// 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; + +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_; +}; + +} // namespace ModuleDFPT + +#endif // DFPT_HAMILT_SHIFT_H diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index ab0256a4d35..cbb96b7e074 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -172,6 +172,30 @@ void DFPT_Pert::real_space_dv(int q_idx, int k_idx, 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; + 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_, 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 { // Invert both ig -> FFT-cell mappings through the (ix,iy,iz) triple: the // rho and wfc bases enumerate different G balls, so their isz encodings // (stick tables) are not interchangeable - only the FFT cell position of @@ -191,10 +215,11 @@ void DFPT_Pert::real_space_dv(int q_idx, int k_idx, 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] * dv_rc[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)); diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index f8708e7476b..b2a6a78c261 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -84,6 +84,17 @@ class DFPT_Pert { std::vector>>& vkb, std::vector>>& dvkb) 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; @@ -141,6 +152,15 @@ class DFPT_Pert { 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); }; diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 720ca4485bd..c66d8c60be5 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. @@ -15,15 +15,28 @@ #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 "source_cell/qlist.h" +#include "source_pw/module_pwdft/stru_fac.h" + +#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_; @@ -32,16 +45,41 @@ 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* 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; + int nqx_ = 1, nqy_ = 1, nqz_ = 1; double conv_thr_ = 1e-8; int max_iter_ = 100; + + 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); }; DFPT_PW::DFPT_PW() : pimpl_(new Impl()) {} @@ -51,25 +89,45 @@ DFPT_PW::~DFPT_PW() { } void DFPT_PW::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* 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; + pimpl_->xc_ = xc; pimpl_->nelec_ = nelec; pimpl_->ecutwfc_ = ecutwfc; pimpl_->dftu_ = dftu; - + 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_->phon_.init(ucell, nullptr, nullptr); + + if (pw_rho != nullptr && pw_wfc != nullptr && sf != nullptr) { + pimpl_->pert_.init(ucell, pw_rho, pw_wfc, *sf); + pimpl_->rho_.init(nspin, nrxx, pw_rho, pw_wfc, ucell.G, "plain", 0.7); + 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); } @@ -81,8 +139,173 @@ 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>>()); + 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) { + ModuleBase::WARNING_QUIT("DFPT_PW::build_occ_kq", + "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)."); + } + + DFPT_KQ_Basis kq; + kq.init(pw_wfc_, q_cart, ik); + const int npw_kq = kq.get_npwk(); + + // reverse map FFT cell -> per-k G index at ikq (the two k balls are + // enumerated per k, only the cell position identifies the G) + std::vector jgl_of_cell(pw_wfc_->nxyz, -1); + for (int jgl = 0; jgl < pw_wfc_->npwk[ikq]; ++jgl) { + const int isz = pw_wfc_->getigl2isz(ikq, jgl); + 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; + jgl_of_cell[(ix * pw_wfc_->ny + iy) * pw_wfc_->nz + iz] = jgl; + } + + const int nbands = gs_psi_.get_nbands(); + for (int m = 0; m < nbands; ++m) { + if (wg_(ikq, m) < 1.0e-8) { + 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 int isz = kq.get_ig2isz(igl); + 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; + const int jgl = jgl_of_cell[(ix * pw_wfc_->ny + iy) * pw_wfc_->nz + iz]; + if (jgl >= 0) { + state[igl] = gs_psi_(ikq, m, jgl); + } + } + 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); + + const int lin_max = data_.get_max_iter(); + const double lin_thr = data_.get_conv_thr(); + + bool converged = false; + double residual = 0.0; + for (int iter = 0; iter < max_iter_ && !converged; ++iter) { + data_.set_current_iter(iter); + + // ---- 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]; + } + } + } + } + + // ---- 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()) { + 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; + } + for (int ib = 0; ib < nbands; ++ib) { + if (wg_(ik, ib) < 1.0e-8) { + 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()) { + 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); + 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_); + data_.add_residual(residual); + converged = (residual < conv_thr_); + } + data_.set_converged(converged); + return residual; +} + void DFPT_PW::run() { - int nq = pimpl_->qlist_.get_nq(); + const int nq = pimpl_->qlist_.get_nq(); DFPT_IrrepData irrep_data(pimpl_->data_); for (int q_idx = 0; q_idx < nq; ++q_idx) { // Special handling for q=0 (uniform electric field responses): @@ -90,37 +313,63 @@ void DFPT_PW::run() { // 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) { + if (q_idx == 0 && pimpl_->data_.get_compute_q0()) { pimpl_->q0_.compute_q0_response(pimpl_->data_); + if (pimpl_->wired()) { + // the dielectric tensor and Born charges of the C6 velocity + // form; the LO-TO term below consumes them + pimpl_->q0_.compute_eps(pimpl_->gs_psi_, pimpl_->wg_, pimpl_->eig_, pimpl_->data_); + pimpl_->q0_.compute_born(pimpl_->gs_psi_, pimpl_->wg_, pimpl_->eig_, pimpl_->data_); + } } - // Per-irrep self-consistent loop: solve only the representative modes - // of each little-group irrep. The irrep decomposition is exposed - // through DFPT_IrrepData (option-2 signatures simulated by the interim - // wrapper); the screening-potential perturbation, the Sternheimer - // solver and the first-order density update are wired in subsequent - // iterations. + // 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); + } + + // 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. const int nirr = irrep_data.get_nirr(q_idx); for (int irrep = 0; irrep < nirr; ++irrep) { irrep_data.set_converged(q_idx, irrep, false); irrep_data.set_current_iter(q_idx, irrep, 0); while (!irrep_data.get_converged(q_idx, irrep) && irrep_data.get_current_iter(q_idx, irrep) < pimpl_->max_iter_) { - // 1. Compute the perturbation of the screening potential - // pimpl_->pert_.build_dv(q_idx, irrep, pimpl_->data_) - // 2. Solve the Sternheimer equation for the representative - // modes of this irrep - // pimpl_->stern_.solve(q_idx, irrep, pimpl_->data_) - // 3. Calculate the first-order density - // pimpl_->rho_.compute_drho(q_idx, irrep, pimpl_->data_) - // 4. Check convergence and iterate until self-consistency - irrep_data.add_residual(q_idx, irrep, 0.0); + if (pimpl_->wired()) { + const int nat = pimpl_->ucell_->nat; + 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); + // 2n+1 accumulation of this converged displacement + pimpl_->phon_.accumulate_electron(q_idx, iat, idir, + pimpl_->gs_psi_, + pimpl_->wg_, + pimpl_->data_); + } + } + irrep_data.add_residual(q_idx, irrep, worst); + } else { + // design-phase skeleton: no bases wired, converge at once + irrep_data.add_residual(q_idx, irrep, 0.0); + } irrep_data.set_converged(q_idx, irrep, true); } } 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 a documented default + // direction (isotropic for cubic crystals; a general q->0 + // direction control arrives with the irrep machinery of stage A) + const double inv = 1.0 / std::sqrt(3.0); + pimpl_->phon_.add_loto(ModuleBase::Vector3(inv, inv, inv), pimpl_->data_); + } } } @@ -156,4 +405,4 @@ void DFPT_PW::set_max_iter(int max_iter) { pimpl_->data_.set_max_iter(max_iter); } -} // namespace ModuleDFPT \ No newline at end of file +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_pw.h b/source/source_pw/module_dfpt/dfpt_pw.h index 7ccaf4903d8..b8a5a84e493 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,46 +9,76 @@ #ifndef DFPT_PW_H #define DFPT_PW_H -#include -#include +#include "source_base/matrix.h" #include "source_cell/unitcell.h" #include "source_psi/psi.h" +#include +#include + class Plus_U; +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, + + 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* 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 (locale initialized, i.e. the LCAO /// orbital files are present). 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); - + void set_qmesh(int nqx, int nqy, int nqz); - + void set_conv_thr(double thr); - + void set_max_iter(int max_iter); - + private: class Impl; Impl* pimpl_; @@ -56,4 +86,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_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index 73cd6b818aa..b8c9232914b 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -194,6 +194,18 @@ void DFPT_Rho::cal_docc(const psi::Psi>& psi, (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) { if (mixer_ == nullptr || pw_rho_ == nullptr) { return; diff --git a/source/source_pw/module_dfpt/dfpt_rho.h b/source/source_pw/module_dfpt/dfpt_rho.h index 656b305fa37..241e9fa011a 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.h +++ b/source/source_pw/module_dfpt/dfpt_rho.h @@ -85,6 +85,12 @@ class DFPT_Rho { 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, diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index 065f419f493..4320d046c16 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -42,6 +42,7 @@ AddTest( ../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 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 index 4dc8f601306..6b89ce90939 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -182,7 +182,9 @@ 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; - dfpt.init(ucell, psi, 1.0, 15.0, nullptr); + // 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 @@ -197,7 +199,8 @@ TEST_F(DFPT_PWRunTest, DielectricAndBornAreExposed) { dfpt.set_qmesh(1, 1, 1); // Gamma-only q mesh psi::Psi> psi; - dfpt.init(ucell, psi, 1.0, 15.0, nullptr); + 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 @@ -219,7 +222,8 @@ TEST_F(DFPT_PWRunTest, DftuReservationWithProviderButUninitializedLocale) Plus_U dftu; dfpt.set_qmesh(1, 1, 1); psi::Psi> psi; - dfpt.init(ucell, psi, 1.0, 15.0, &dftu); + dfpt.init(ucell, psi, nullptr, nullptr, nullptr, std::vector(), + ModuleBase::matrix(), ModuleBase::matrix(), nullptr, 1.0, 15.0, &dftu); EXPECT_TRUE(dfpt.get_with_u()); EXPECT_FALSE(dfpt.get_u_active()); dfpt.run(); From a0047421b844cc1a1e770cd9d073c17c946406c0 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 17 Aug 2026 22:19:09 +0800 Subject: [PATCH 14/50] Fix: DFPT screening-channel calibration (q=0 completion, XC central difference, per-displacement reset) Three fixes verified against finite-difference references on the diamond two-atom smoke case (optical 742.367x3 cm^-1 vs FD ~742, acoustic 6.40x3, ASR residual 3.1e-6, off-irrep elements ~1e-11): 1. compute_drho: replace the in-place G-space Hermitian completion (double-processing each +-G pair, breaking Hermiticity and leaking a ~1.25x uniform overshoot) with a real-space 2 Re a(r) presymmetrization before real2recip; one-sided sticks whose -G falls outside the sphere now also complete correctly. 2. XC_First_Order_FDM: the forward difference Vxc[rho+drho]-Vxc[rho] carries a curvature term ~Vxc''*drho^2/2 that leaks a spurious A1 component into v_sc (violating the A1xT2xA1 selection rule by 1.7e-2 Ry/bohr) and destabilizes plain mixing at beta=0.7; use an eta=1e-6 central difference instead (leak ~1e-11, default mixing converges). 3. solve_displacement: zero the stored drho_g when (re)entering a displacement so the previous response (or diverged leftovers) cannot leak into the first screening iteration. Also includes the design-phase debug instrumentation used for the diagnosis (DFPT_DEBUG/PTCHK/DYNCHK/MDBG/dump blocks, DFPT_MIX_BETA env knob) and removes the VQCHK block that read PARAM.globalv.dq/nqx (governance: keep the PR-level global dependency budget non-increasing). Verification: ctest 10/10 (build/, MODULE_DFPT* + little_group + klist); governance --staged clean except advisory warnings; smoke rerun after VQCHK removal reproduces frequencies. --- source/source_esolver/esolver_dfpt_pw.cpp | 147 +++++++++- .../module_dfpt/PLAN_dfpt_implementation.md | 7 + .../module_dfpt/dfpt_hamilt_shift.cpp | 54 ++++ .../source_pw/module_dfpt/dfpt_hamilt_shift.h | 12 +- source/source_pw/module_dfpt/dfpt_pert.cpp | 63 +++-- source/source_pw/module_dfpt/dfpt_pert.h | 21 +- source/source_pw/module_dfpt/dfpt_phon.cpp | 46 +++- source/source_pw/module_dfpt/dfpt_pw.cpp | 250 +++++++++++++++++- source/source_pw/module_dfpt/dfpt_rho.cpp | 87 ++++-- source/source_pw/module_dfpt/dfpt_rho.h | 3 +- 10 files changed, 630 insertions(+), 60 deletions(-) diff --git a/source/source_esolver/esolver_dfpt_pw.cpp b/source/source_esolver/esolver_dfpt_pw.cpp index f7fea6eb9c0..56f8e0688dd 100644 --- a/source/source_esolver/esolver_dfpt_pw.cpp +++ b/source/source_esolver/esolver_dfpt_pw.cpp @@ -11,12 +11,15 @@ #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 namespace { @@ -54,27 +57,62 @@ class XC_First_Order_FDM : public ModuleDFPT::XC_First_Order std::vector>& dvxc_r) const override { const int nrxx = veff_1_.nc; - // real part: V_xc[rho0 + Re drho] - V_xc[rho0] + 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] = drho_r[ir].real(); + 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] = veff_1_(0, ir); + dvxc_r[ir] = (v_plus[ir] - veff_1_(0, ir)) / (2.0 * eta); } - // imaginary part: V_xc[rho0 + Im drho] - V_xc[rho0] + // imaginary part: same central difference on Im drho for (int ir = 0; ir < nrxx; ++ir) { - chg1_->rho[0][ir] = drho_r[ir].imag(); + 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) * veff_1_(0, 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); } } @@ -217,6 +255,65 @@ void ESolver_DFPT_PW::init_dfpt(UnitCell& ucell) // 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); + 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_->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); @@ -225,6 +322,44 @@ void ESolver_DFPT_PW::init_dfpt(UnitCell& ucell) void ESolver_DFPT_PW::run_post_process(UnitCell& ucell) { ModuleBase::TITLE("ESolver_DFPT_PW", "run_post_process"); + + if (dfpt_ == nullptr) + { + return; + } + // design-phase validation output (single-rank runs); the io layer + // integration lands with the data-layer consolidation stage + const std::vector freqs = dfpt_->get_phonon_freq(0); + std::cout << " DFPT phonon frequencies at q #0 (cm^-1):" << std::endl; + for (size_t im = 0; im < freqs.size(); ++im) + { + std::cout << " mode " << im << " : " << freqs[im] << " cm^-1" << std::endl; + } + const ModuleBase::matrix& eps = dfpt_->get_dielectric_tensor(); + 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); + 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_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index a512f0efc28..729ccbdecb8 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -175,5 +175,12 @@ - `read_inp_sys.cpp`:esolver_types 合法值加 `"dfpt"` + 注释/description 更新;`docs/parameters.yaml` + `docs/advanced/input_files/input-main.md` 同步 - 验证:`cmake --build` esolver/abacus_pw_para/12 测试目标全绿;ctest 12/12(CELL 4 + DFPT 8);`abacus_pw_para -h esolver_type` 显示 dfpt 条目;`--version` v3.11.0-beta8;治理仅 determine_type 工厂 1 处豁免 ERROR + 既有 header/docs WARNING - 待办(随 B/前置验证):金刚石端到端声子/ε∞/Z* 对照、`--check-input` 从有效算例目录验证 +- [x] 校准:屏蔽通道三处修复(金刚石 2 原子 smoke,24³ rho 网格,NC PP,Γ 点) + - FD/Ewald 锁定基线:e11=+0.08056、e12=−0.08059 Ry/bohr²(预质量 0.0028685/−0.0028701),光学 ~742 cm⁻¹;GS 力 FD 交叉验证 dV 装配(F_x 偏差 0.02%) + - 修复 1(dfpt_rho.cpp compute_drho):q=0 Hermitian 完成的 in-place `drho_g[ig] += conj(drho_g[gm])` 逐点双重处理 ±G 对(第二次访问读到已更新的第一项)→ 结果破坏 Hermitian 性,实空间重建混入 Re a(r) 寄生分量(均匀 ~1.25 过冲、对称违反被放大 1.3-1.8%、A1 投影 3.7%);最终实现 = 实空间 `2 Re a(r)` 预对称化后再 real2recip(实数组 FFT 本征 Hermitian,单边 stick(−G 不在球内)亦获正确完成值,替代 G 空间逐点镜像) + - 修复 2(esolver_dfpt_pw.cpp XC_First_Order_FDM):前向差分 `Vxc[ρ+δρ]−Vxc[ρ]` 的曲率项 ½Vxc″δρ²(T2⊗T2⊃A1)向 v_sc 泄漏寄生 A1(band0 ⟨dv_sc⟩=+0.0173 违反 A1⊗T2⊗A1 选择定则、占据三重态迹 +0.052)且二次非线性反馈使混合迭代 β=0.7 超指数暴走;改 η=1e-6 中心差分(Re/Im 各一对 cal_v_eff 探测)后泄漏 ~1e-11,默认 β=0.7 恢复收敛 + - 修复 3(dfpt_pw.cpp solve_displacement):`reset_mixing` 只清混合器内部态,data 层 `drho_g` 残留上一位移响应(含发散残渣)泄漏进新位移首迭代 v_sc;进入位移时同步清零 + - 修复后(默认 β=0.7,~76 s):光学 742.367×3(FD ~742)、声学 6.40×3(ASR:e11+e12=3.1e-6)、e11=0.00286804(目标 0.0028685)、e12=−0.00286494(目标 −0.0028701)、非 irrep 元 ~1e-11、收敛 drho 小群违反 0.000000/A1 投影 5e-6(对称性精确);裸响应(β=0.001 dump)小群违反 ~0.1% 确认裸链(Sternheimer/dV/dψ)干净 + - 遗留:迭代后期慢漂移(|drho| 稳定 0.0154 后缓慢爬至 0.043@iter99,不触发收敛旗标;力矩阵不受污染但 drho_r manifest 受污染——FD cmp 比率 3.98 为漂移伪影,修复前干净态比率 1.0285/cos 0.9976);ε∞/Z* 打印为空(随 B 阶段);调试插桩(PTCHK/DYNCHK/MDBG dump/VKBCHK/drho dump/DFPT_MIX_BETA env)收尾节点统一清理评审 - [ ] B 数据层收编 - [ ] A irrep 分解 diff --git a/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp b/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp index bbf1346abf7..51ec2610086 100644 --- a/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp +++ b/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp @@ -64,6 +64,7 @@ DFPT_HamiltShift::~DFPT_HamiltShift() {} void DFPT_HamiltShift::set_context(const ModuleBase::Vector3& q_cart, int k_idx) { kq_.init(pw_wfc_, q_cart, k_idx); + ik_cache_ = k_idx; const int npw = kq_.get_npwk(); // rho ig -> shared FFT-cell reverse map, then k+q -> rho through the @@ -174,4 +175,57 @@ void DFPT_HamiltShift::apply(const std::complex* x, std::complex } } +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 index b499482be21..bd5aaa35ea5 100644 --- a/source/source_pw/module_dfpt/dfpt_hamilt_shift.h +++ b/source/source_pw/module_dfpt/dfpt_hamilt_shift.h @@ -60,10 +60,18 @@ class DFPT_HamiltShift : public DFPT_Stern::LinearOperator { /// 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; @@ -90,6 +98,8 @@ class DFPT_HamiltShift : public DFPT_Stern::LinearOperator { mutable std::vector> x_r_; mutable std::vector> becp_; mutable std::vector> dbecp_; + + int ik_cache_ = 0; }; } // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index cbb96b7e074..6f4d8b4c15e 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -125,14 +125,15 @@ void DFPT_Pert::dVloc_dtau(int atom_idx, int dir, } const double g_bohr2 = w2 * ucell_->tpiba2; const double vloc = vloc_at_g(it, g_bohr2); - // GS phase convention (stru_fac.cpp / get_sk): 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); + // 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 (Delta+q)_dir * Vloc * exp(i (Delta+q).tau) + // 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]); + std::complex(0.0, -1.0) * (ucell_->tpiba * w[dir]); dv[ig] = iw_dir * vloc * phase; } } @@ -258,6 +259,33 @@ void DFPT_Pert::apply_dv(int q_idx, int k_idx, const psi::Psi (per displaced atom) std::vector>> dv_psi_nl; dVnl_dtau(atom_idx, dir, q_cart, psi, k_idx, dv_psi_nl); + if (getenv("DFPT_MDBG") != nullptr && atom_idx < 2 && k_idx == 0) { + static int done[2] = {0, 0}; + if (!done[atom_idx]) { + done[atom_idx] = 1; + const int npw_dbg = psi.get_nbasis(); + const int nb_dbg = psi.get_nbands(); + for (int ib = 0; ib < nb_dbg; ++ib) { + for (int m = 0; m < nb_dbg; ++m) { + std::complex dl(0.0, 0.0); + std::complex dn(0.0, 0.0); + for (int ig = 0; ig < npw_dbg; ++ig) { + if (static_cast(dv_psi[ib].size()) == npw_dbg) { + dl += std::conj(psi(k_idx, m, ig)) * dv_psi[ib][ig]; + } + if (dv_psi_nl.size() == static_cast(nb_dbg) + && static_cast(dv_psi_nl[ib].size()) == npw_dbg) { + dn += std::conj(psi(k_idx, m, ig)) * dv_psi_nl[ib][ig]; + } + } + std::cout << "MDBG atom=" << atom_idx << " n=" << ib << " m=" << m + << " loc=(" << dl.real() << "," << dl.imag() << ")" + << " nl=(" << dn.real() << "," << dn.imag() << ")" + << std::endl; + } + } + } + } 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()) { @@ -379,11 +407,14 @@ void DFPT_Pert::build_vkb(int it, int ia, 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 - const double gmag = gnorm / ucell_->tpiba; // |G|, 2*pi/lat0 - const double ylm = (gmag > 1.0e-10) ? real_ylm(l, mr, G * (1.0 / gmag)) : 0.0; + // 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); - // same GS phase convention as dVloc_dtau: exp(i 2pi (gk.tau)) - const double arg = ModuleBase::TWO_PI * (G * tau); + // 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; } @@ -461,10 +492,10 @@ void DFPT_Pert::build_vkb_dk(int it, int ia, int dir, 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 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; + 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) { @@ -607,7 +638,9 @@ void DFPT_Pert::dVnl_dtau(int atom_idx, int dir, term_b[igl] = vnl_dpsi; } for (int igl = 0; igl < npwk_kq; ++igl) { - dv_psi[iband][igl] = term_a[igl] - term_b[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]; } } } @@ -655,9 +688,9 @@ void DFPT_Pert::d2vloc_r(int atom_idx, int da, int db, continue; } const double vloc = vloc_at_g(it, w2 * ucell_->tpiba2); - const double arg = ModuleBase::TWO_PI * (w * tau); + const double arg = -ModuleBase::TWO_PI * (w * tau); const std::complex phase(std::cos(arg), std::sin(arg)); - // (i w_da)(i w_db) = -w_da w_db + // (-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)); diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index b2a6a78c261..add12f04c3d 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -84,6 +84,9 @@ class DFPT_Pert { 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 @@ -114,28 +117,26 @@ class DFPT_Pert { /// 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) * ... - /// The sign/coefficient convention is the exact derivative of the local - /// potential with respect to the atomic displacement (see source; the unit - /// test checks it against a finite difference of the full potential incl. - /// the atomic phase). + /// * 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 - /// dVnl/dtau_a |psi> = i (k+q+G'')_a * (Vnl |psi>) - Vnl[ i (k+G')_a |psi> ] + /// 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); - /// 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; /// 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 diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index 6577d18c7a5..668ec13aaa3 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include namespace ModuleDFPT { @@ -358,6 +360,7 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, // relies on them. pert_->build_dv(q_idx, iat, idir, data); std::complex cross(0.0, 0.0); + const bool dbg2 = (getenv("DFPT_DEBUG") != nullptr); for (int ik = 0; ik < nk; ++ik) { pert_->apply_dv(q_idx, ik, psi, data); for (int ib = 0; ib < nbands; ++ib) { @@ -376,7 +379,13 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, cross += wg(ik, ib) * dot; } } - dynmat_accum_(rowb, cola) += 2.0 * cross; + dynmat_accum_(rowb, cola) += 2.0 * cross + / std::sqrt(ucell_->atoms[ucell_->iat2it[atom_idx]].mass + * ucell_->atoms[ucell_->iat2it[iat]].mass); + if (dbg2) { + std::cout << "DYNCHK term2 rowb=" << rowb << " cola=" << cola + << " 2cross=" << 2.0 * cross.real() << std::endl; + } // ---- same-atom anharmonic term ---- if (iat == atom_idx && cola >= rowb) { @@ -387,6 +396,8 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, } std::vector>> chi; std::complex d2sum(0.0, 0.0); + std::complex d2sum_loc(0.0, 0.0); + std::complex d2sum_nl(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)); @@ -423,14 +434,27 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, std::fill(x_r.begin(), x_r.end(), std::complex(0.0, 0.0)); } std::complex expect(0.0, 0.0); + std::complex expect_loc(0.0, 0.0); + std::complex expect_nl(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]; + expect_loc += std::conj(u_r[ir]) * u_r[ir] * dv2_r[ir]; + expect_nl += std::conj(u_r[ir]) * x_r[ir]; } d2sum += wg(ik, ib) * expect / static_cast(pw_rho_->nxyz); + d2sum_loc += wg(ik, ib) * expect_loc / static_cast(pw_rho_->nxyz); + d2sum_nl += wg(ik, ib) * expect_nl / static_cast(pw_rho_->nxyz); } } - dynmat_accum_(rowb, cola) += d2sum; + dynmat_accum_(rowb, cola) += d2sum + / ucell_->atoms[ucell_->iat2it[atom_idx]].mass; + if (dbg2) { + std::cout << "DYNCHK d2 rowb=" << rowb << " cola=" << cola + << " d2sum=" << d2sum.real() + << " loc=" << d2sum_loc.real() + << " nl=" << d2sum_nl.real() << std::endl; + } } } } @@ -460,6 +484,24 @@ void DFPT_Phon::assemble(int q_idx, DFPT_PW_Data& data) { ion_ion(data.get_qvec(q_idx), dyn); } if (accum_q_ == q_idx && dynmat_accum_.nr == nat3) { + if (getenv("DFPT_DEBUG") != nullptr) { + std::cout << "DYNCHK ionic matrix (Ry/bohr^2/amu):" << std::endl; + for (int i = 0; i < nat3; ++i) { + std::cout << "DYNCHK ion row " << i << ":"; + for (int j = 0; j < nat3; ++j) { + std::cout << " " << dyn(i, j).real(); + } + std::cout << std::endl; + } + std::cout << "DYNCHK electronic accum matrix:" << std::endl; + for (int i = 0; i < nat3; ++i) { + std::cout << "DYNCHK ele row " << i << ":"; + for (int j = 0; j < nat3; ++j) { + std::cout << " " << dynmat_accum_(i, j).real(); + } + std::cout << std::endl; + } + } for (int i = 0; i < nat3; ++i) { for (int j = 0; j < nat3; ++j) { dyn(i, j) += dynmat_accum_(i, j); diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index c66d8c60be5..61e0f92970e 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -18,12 +18,15 @@ #include "dfpt_hamilt_shift.h" #include "dfpt_kq_basis.h" #include "source_base/constants.h" +#include #include "source_base/global_function.h" #include "source_cell/qlist.h" #include "source_pw/module_pwdft/stru_fac.h" #include #include +#include +#include #include #include @@ -66,6 +69,7 @@ class DFPT_PW::Impl { ///< 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; double conv_thr_ = 1e-8; @@ -120,7 +124,18 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, if (pw_rho != nullptr && pw_wfc != nullptr && sf != nullptr) { pimpl_->pert_.init(ucell, pw_rho, pw_wfc, *sf); - pimpl_->rho_.init(nspin, nrxx, pw_rho, pw_wfc, ucell.G, "plain", 0.7); + // plain-mixing coefficient: the converged Jacobian of the response + // problem can have mildly negative eigenvalues (LDA kernel), so the + // coefficient must stay below 2 / (1 + |lambda_min|); the env knob + // is a design-phase calibration aid + double mix_beta = 0.7; + 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; + } + } + pimpl_->rho_.init(nspin, nrxx, pw_rho, pw_wfc, ucell.G, "plain", mix_beta); pimpl_->phon_.init(ucell, pw_rho, &pimpl_->pert_); pimpl_->q0_.init(ucell, pw_rho, pw_wfc, &pimpl_->pert_); delete pimpl_->hamilt_; @@ -142,6 +157,7 @@ bool DFPT_PW::get_u_active() const { 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) { @@ -167,6 +183,7 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { "the DFPT q mesh must be commensurate with the " "k mesh (and inside the first Brillouin zone)."); } + ikq_of_k_[ik] = ikq; DFPT_KQ_Basis kq; kq.init(pw_wfc_, q_cart, ik); @@ -222,12 +239,18 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { 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); for (int iter = 0; iter < max_iter_ && !converged; ++iter) { data_.set_current_iter(iter); @@ -256,11 +279,34 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } } } + 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; + if (getenv("DFPT_MDBG") != nullptr) { + static int dump_cnt = 0; + std::ofstream df("/tmp/opencode/drho_iters/it" + + std::to_string(dump_cnt++) + ".bin", + std::ios::binary); + for (int ig = 0; ig < pw_rho_->npw; ++ig) { + df.write(reinterpret_cast(&drho_in_g[ig]), + sizeof(std::complex)); + } + } + } } // ---- 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) @@ -271,6 +317,35 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { 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 (wg_(ik, ib) < 1.0e-8) { @@ -279,6 +354,13 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { 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> @@ -289,6 +371,19 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { 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); } } @@ -301,6 +396,159 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { converged = (residual < conv_thr_); } data_.set_converged(converged); + + // design-phase validation: dump converged self-consistent drho on the + // shared real-space grid for direct comparison with finite differences + if (dbg && q_idx == 0 && iat == 0 && idir == 0) { + const std::vector> dg = data_.get_drho_g(q_idx, 0); + std::vector> dr(pw_rho_->nrxx, std::complex(0.0, 0.0)); + if (static_cast(dg.size()) == pw_rho_->npw) { + pw_rho_->recip2real(dg.data(), dr.data()); + } + std::ofstream df("/tmp/opencode/drho_dfpt.dat"); + df << pw_rho_->nx << " " << pw_rho_->ny << " " << pw_rho_->nz << "\n"; + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + df << dr[ir].real() << " " << dr[ir].imag() << "\n"; + } + } + // design-phase validation: 8-band perturbation-theory term2 cross-check + // for the first displacement of the first atom (q = 0, ik = 0 only) + if (dbg && q_idx == 0 && iat == 0 && idir == 0 && nk > 0 && nbands > 4) { + // gauge check: must vanish for occupied k (Sternheimer + // gauge); a nonzero admixture pollutes term2 via occ-occ dV elements + for (int n = 0; n < 4; ++n) { + const std::vector>& dps = data_.get_dpsi(q_idx, 0, n); + const int npwg = gs_psi_.get_nbasis(); + if (static_cast(dps.size()) != npwg) { + continue; + } + for (int k = 0; k < 4; ++k) { + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npwg; ++ig) { + dot += std::conj(gs_psi_(0, k, ig)) * dps[ig]; + } + std::cout << "PTCHK gauge n=" << n << " k=" << k + << " =" << dot << std::endl; + } + } + // stash the solved dpsi (apply_dv below reuses the slots) + std::vector>> solved(nbands); + for (int ib = 0; ib < nbands; ++ib) { + solved[ib] = data_.get_dpsi(q_idx, 0, ib); + } + const int npw = gs_psi_.get_nbasis(); + // M[a][m][n] = + std::vector>>> mat( + 2, std::vector>>( + nbands, std::vector>(nbands, std::complex(0.0, 0.0)))); + for (int a = 0; a < 2; ++a) { + pert_.build_dv(q_idx, a, idir, data_); + pert_.apply_dv(q_idx, 0, gs_psi_, data_); + for (int n = 0; n < nbands; ++n) { + const std::vector> dvpsi = data_.get_dpsi(q_idx, 0, n); + if (static_cast(dvpsi.size()) != npw) { + continue; + } + for (int m = 0; m < nbands; ++m) { + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npw; ++ig) { + dot += std::conj(gs_psi_(0, m, ig)) * dvpsi[ig]; + } + mat[a][m][n] = dot; + } + } + } + // PT term2 with the 4 empty bands only, b = atom 0 (solved dir) + for (int a = 0; a < 2; ++a) { + std::complex pt(0.0, 0.0); + for (int n = 0; n < 4; ++n) { + const double w = wg_(0, n); + if (w < 1.0e-8) { + continue; + } + for (int m = 4; m < nbands; ++m) { + pt += w * std::conj(mat[a][m][n]) * mat[0][m][n] + / (eig_(0, n) - eig_(0, m)); + } + } + std::cout << "PTCHK term2(a=" << a << ",b=0) PT-4empty=" << 2.0 * pt << std::endl; + } + // element dump: M[a][m][n] for m empty, n occupied + for (int a = 0; a < 2; ++a) { + for (int m = 4; m < nbands; ++m) { + for (int n = 0; n < 4; ++n) { + std::cout << "PTCHK M a=" << a << " m=" << m << " n=" << n + << " (" << mat[a][m][n].real() << "," << mat[a][m][n].imag() << ")" + << std::endl; + } + } + } + // solved-dpsi band projections + for (int n = 0; n < 4; ++n) { + for (int m = 0; m < nbands; ++m) { + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npw; ++ig) { + dot += std::conj(gs_psi_(0, m, ig)) * solved[n][ig]; + } + std::cout << "PTCHK proj n=" << n << " m=" << m + << " =(" << dot.real() << "," << dot.imag() << ")" + << std::endl; + } + } + // Hellmann-Feynman check targets: vs FD of eps_n + for (int a = 0; a < 2; ++a) { + for (int n = 0; n < 4; ++n) { + std::cout << "PTCHK HF a=" << a << " n=" << n + << " =" << mat[a][n][n] << std::endl; + } + } + // design-phase validation: rebuild the converged screened potential + // and compare de_code(n) = + against FD eigenvalue + // derivatives (localizes response errors in the screening channel) + { + const std::vector> dg_in = data_.get_drho_g(q_idx, 0); + if (static_cast(dg_in.size()) == pw_rho_->npw) { + std::vector> vha_g; + rho_.v_hartree_q(q_cart, dg_in, vha_g); + std::vector> v_sc_r2(pw_rho_->nrxx, std::complex(0.0, 0.0)); + std::vector> vh_r(pw_rho_->nrxx); + pw_rho_->recip2real(vha_g.data(), vh_r.data()); + std::vector> vx_r; + if (xc_ != nullptr) { + std::vector> ar(pw_rho_->nrxx); + pw_rho_->recip2real(dg_in.data(), ar.data()); + xc_->apply(ar, vx_r); + } + for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { + v_sc_r2[ir] = vh_r[ir]; + if (static_cast(vx_r.size()) == pw_rho_->nrxx) { + v_sc_r2[ir] += vx_r[ir]; + } + } + std::vector>> dvsc; + pert_.apply_vr(q_idx, 0, v_sc_r2, gs_psi_, q_cart, dvsc); + for (int n = 0; n < nbands; ++n) { + const std::vector>& v = dvsc[n]; + if (static_cast(v.size()) != npw) { + continue; + } + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npw; ++ig) { + dot += std::conj(gs_psi_(0, n, ig)) * v[ig]; + } + std::cout << "PTCHK de n=" << n + << " =(" << dot.real() << "," << dot.imag() << ")" + << " de_code=" << (mat[0][n][n] + dot).real() << std::endl; + } + } + } + // restore the solved dpsi + for (int ib = 0; ib < nbands; ++ib) { + if (!solved[ib].empty()) { + data_.set_dpsi(q_idx, 0, ib, solved[ib]); + } + } + } return residual; } diff --git a/source/source_pw/module_dfpt/dfpt_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index b8c9232914b..e01bb106282 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -115,12 +115,33 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, } } pw_rho_->recip2real(d_recip.data(), d_r.data()); + // same normalization as the GS density accumulation + // (elecstate_pw.cpp rhoBandK: w1 = wg / omega) + const double w1 = w / pw_rho_->omega; for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { - a_r[ir] += w * std::conj(u_r[ir]) * d_r[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; the physical (real) response density also contains + // the du_n u_n^* piece, whose coefficients are conj(a_{-G}). At q = 0 + // both harmonics coincide: the response is Delta rho = 2 Re a(r), so + // symmetrize the real-space 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(2.0 * 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()); @@ -147,21 +168,28 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, } data.set_drho_g(q_idx, 0, drho_g); - // real-space manifest density 2 Re[e^{i q r} A(r)], rebuilt from the - // (conservation-projected) coefficients so both storages agree + // 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); - 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)); + 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)); + } } } } @@ -241,21 +269,32 @@ void DFPT_Rho::mix_drho(int q_idx, DFPT_PW_Data& data) { drho_in_[q_idx][0] = mixed; data.set_drho_g(q_idx, 0, mixed); - // rebuild the real-space manifest from the mixed coefficients + // 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 ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); + 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); - 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)); + 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)); + } } } } diff --git a/source/source_pw/module_dfpt/dfpt_rho.h b/source/source_pw/module_dfpt/dfpt_rho.h index 241e9fa011a..cde8bf0beec 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.h +++ b/source/source_pw/module_dfpt/dfpt_rho.h @@ -41,7 +41,8 @@ class XC_First_Order { /// 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 fully overwritten. + /// 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; }; From 57f74153baac5585ab80f84b579bda09c9d7af5f Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 17 Aug 2026 22:55:11 +0800 Subject: [PATCH 15/50] Fix: DFPT plain-mixing default beta 0.7 -> 0.4 (small-G Coulomb stiffness) The late-iteration divergence diagnosed in the diamond smoke case is a plain-mixing stability issue, not a physics bug: residual stalls at 5e-5 then grows at exactly 1.2765x/iter while the iterate norm stays constant (junk direction orthogonal to the physical component). The eigenmode is a real Hermitian A1 breathing mode on the smallest G shells ({200} 6-vector equal real amplitudes + {111} 8-vector +-pi/4 phases). A homogeneous probe (inject the pure A1 trial, drop dV_ext from the rhs, measure the one-iteration linear map; DFPT_JPROBE / DFPT_JPROBE_NOXC) gives lambda_A1 = -2.229 (Hartree-only -3.180, XC reduces it to -2.23) i.e. the Coulomb stiffness 4pi/G^2 at small G. Plain mixing needs beta < 2/(1+|lambda_min|) ~ 0.62; the physical T2 mode (lambda = -1.42, less small-G head content) happened to converge at 0.7, which is why the fixed point was correct while the A1 channel diverged (also explains the earlier beta=0.3 convergence and the polluted drho manifest). Default beta is now 0.4 (margin up to |lambda| ~ 5). Verification at default settings: all six displacements exit via the convergence flag (~38 iterations average, 228 total), frequencies identical to the beta=0.7 forced run (optical 742.367 x3, acoustic 6.40 x3; fixed point independent of beta), ele rows unchanged (e11 0.00286804 vs target 0.0028685, e12 -0.00286494 vs -0.0028701), converged drho manifest now clean against the finite-difference reference (ratio 0.99994, cos 0.9993, 3.8% pointwise). ctest 10/10 (MODULE_DFPT* + little_group + klist); governance --staged clean except advisory warnings. Proper fix is a Kerker-type preconditioned mixer, noted for the B-phase follow-up. Also adds env-gated design-phase diagnostics used for the diagnosis: per-iteration residual print, MDBG dumps of drho/v_sc/v_ha/gcar, and the JPROBE homogeneous-probe path. --- .../module_dfpt/PLAN_dfpt_implementation.md | 4 +- source/source_pw/module_dfpt/dfpt_pw.cpp | 118 ++++++++++++++++-- 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 729ccbdecb8..3b260413056 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -181,6 +181,8 @@ - 修复 2(esolver_dfpt_pw.cpp XC_First_Order_FDM):前向差分 `Vxc[ρ+δρ]−Vxc[ρ]` 的曲率项 ½Vxc″δρ²(T2⊗T2⊃A1)向 v_sc 泄漏寄生 A1(band0 ⟨dv_sc⟩=+0.0173 违反 A1⊗T2⊗A1 选择定则、占据三重态迹 +0.052)且二次非线性反馈使混合迭代 β=0.7 超指数暴走;改 η=1e-6 中心差分(Re/Im 各一对 cal_v_eff 探测)后泄漏 ~1e-11,默认 β=0.7 恢复收敛 - 修复 3(dfpt_pw.cpp solve_displacement):`reset_mixing` 只清混合器内部态,data 层 `drho_g` 残留上一位移响应(含发散残渣)泄漏进新位移首迭代 v_sc;进入位移时同步清零 - 修复后(默认 β=0.7,~76 s):光学 742.367×3(FD ~742)、声学 6.40×3(ASR:e11+e12=3.1e-6)、e11=0.00286804(目标 0.0028685)、e12=−0.00286494(目标 −0.0028701)、非 irrep 元 ~1e-11、收敛 drho 小群违反 0.000000/A1 投影 5e-6(对称性精确);裸响应(β=0.001 dump)小群违反 ~0.1% 确认裸链(Sternheimer/dV/dψ)干净 - - 遗留:迭代后期慢漂移(|drho| 稳定 0.0154 后缓慢爬至 0.043@iter99,不触发收敛旗标;力矩阵不受污染但 drho_r manifest 受污染——FD cmp 比率 3.98 为漂移伪影,修复前干净态比率 1.0285/cos 0.9976);ε∞/Z* 打印为空(随 B 阶段);调试插桩(PTCHK/DYNCHK/MDBG dump/VKBCHK/drho dump/DFPT_MIX_BETA env)收尾节点统一清理评审 + - 后期漂移根因(本轮确诊):残差降至 5e-5 后指数增长(1.27×/iter)、|in| 恒定而 out 偏离 → 垃圾方向与物理分量正交、混合映射本征值 μ=1.2765 恒定(纯本征模);本征模身份 = {200} 壳 6 矢等幅实系数 + {111} 壳 8 矢 ±π/4 相位的 Hermitian 实 A1 呼吸模(seed ~1e-6 舍入级);均匀探针实验(DFPT_JPROBE:注入纯 A1 模 + rhs 去 dV_ext 单迭代直测线性映射)给出 λ_A1 = −2.229(Hartree-only −3.180,XC 削减到 −2.23)——非符号 bug,是最小 G 壳的 Coulomb 刚性(4π/G² 硬核):plain mixing 收敛条件 −2/β+1<λ 要求 β<0.62,物理 T2 模 λ=−1.42(小 G 头部含量少)在 β=0.7 恰好可收敛,故固定点正确而 A1 通道发散;β=0.4 时 μ_A1=−0.29 稳定 + - 修复:默认 mix_beta 0.7→0.4(注释记录测得的 |λ|~2.2 与 β 上界 2/(1+|λ_min|),留裕量至 |λ|~5);DFPT_MIX_BETA env 旋钮保留;β=0.4 时 6 位移全部经收敛旗标退出(平均 ~38 iter,总 228),频率/ele 矩阵与 β 无关逐位一致(固定点正确性再验证),收敛 drho manifest 干净(|FD| 比率 0.99994、cos 0.9993、逐点相对差 3.8%);后续正解是 Kerker 型预条件混合(随 B 阶段排期) + - ε∞/Z* 打印为空(随 B 阶段);调试插桩(PTCHK/DYNCHK/MDBG/JPROBE dump/VKBCHK/drho dump/DFPT_MIX_BETA env)收尾节点统一清理评审 - [ ] B 数据层收编 - [ ] A irrep 分解 diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 61e0f92970e..54d10d97724 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -124,11 +124,13 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, if (pw_rho != nullptr && pw_wfc != nullptr && sf != nullptr) { pimpl_->pert_.init(ucell, pw_rho, pw_wfc, *sf); - // plain-mixing coefficient: the converged Jacobian of the response - // problem can have mildly negative eigenvalues (LDA kernel), so the - // coefficient must stay below 2 / (1 + |lambda_min|); the env knob - // is a design-phase calibration aid - double mix_beta = 0.7; + // 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|); 0.4 keeps margin up to |lambda| ~ 5; the + // env knob is a design-phase calibration aid + double mix_beta = 0.4; if (const char* env_beta = getenv("DFPT_MIX_BETA")) { const double parsed = atof(env_beta); if (parsed > 0.0 && parsed <= 1.0) { @@ -247,6 +249,11 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { const int lin_max = data_.get_max_iter(); const double lin_thr = data_.get_conv_thr(); + // design-phase homogeneous probe: inject a pure A1 trial density on the + // {200}/{111} shells, drop the external perturbation from the rhs, run + // one iteration and dump the linear map output M * trial + const bool jprobe = (getenv("DFPT_JPROBE") != nullptr); + const bool jprobe_noxc = (getenv("DFPT_JPROBE_NOXC") != nullptr); bool converged = false; double residual = 0.0; @@ -254,6 +261,32 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { for (int iter = 0; iter < max_iter_ && !converged; ++iter) { data_.set_current_iter(iter); + if (jprobe && iter == 0) { + std::vector> trial(pw_rho_->npw, + std::complex(0.0, 0.0)); + double nrm = 0.0; + for (int ig = 0; ig < pw_rho_->npw; ++ig) { + const ModuleBase::Vector3 gc = pw_rho_->gcar[ig]; + const double g2 = gc * gc; + const int nax = (std::abs(gc.x) > 1.0e-6) + + (std::abs(gc.y) > 1.0e-6) + + (std::abs(gc.z) > 1.0e-6); + if (std::abs(g2 - 4.0) < 1.0e-9 && nax == 1) { + trial[ig] = std::complex(1.0, 0.0); + nrm += 1.0; + } else if (std::abs(g2 - 3.0) < 1.0e-9 && nax == 3) { + const double sgn = (gc.x * gc.y * gc.z > 0.0) ? 1.0 : -1.0; + trial[ig] = std::complex(0.6218, -0.6218 * sgn); + nrm += 2.0 * 0.6218 * 0.6218; + } + } + const double inv = 1.0 / std::sqrt(nrm); + for (size_t i = 0; i < trial.size(); ++i) { + trial[i] *= inv; + } + data_.set_drho_g(q_idx, 0, trial); + } + // ---- 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 @@ -268,7 +301,7 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { for (int ir = 0; ir < nrxx; ++ir) { v_sc_r[ir] = vh_r[ir]; } - if (xc_ != nullptr) { + if (xc_ != nullptr && !jprobe_noxc) { std::vector> a_r(nrxx); pw_rho_->recip2real(drho_in_g.data(), a_r.data()); std::vector> b_r; @@ -278,6 +311,26 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { v_sc_r[ir] += b_r[ir]; } } + if (getenv("DFPT_MDBG") != nullptr) { + static int vdbg_cnt = 0; + std::ofstream vf("/tmp/opencode/drho_iters/vsc_" + + std::to_string(vdbg_cnt++) + ".bin", + std::ios::binary); + for (int ir = 0; ir < nrxx; ++ir) { + vf.write(reinterpret_cast(&v_sc_r[ir]), + sizeof(std::complex)); + } + // channel split: rebuild each part for the same input + std::vector> vh_only(nrxx); + pw_rho_->recip2real(dv_ha_g.data(), vh_only.data()); + std::ofstream hf("/tmp/opencode/drho_iters/vha_" + + std::to_string(vdbg_cnt - 1) + ".bin", + std::ios::binary); + for (int ir = 0; ir < nrxx; ++ir) { + hf.write(reinterpret_cast(&vh_only[ir]), + sizeof(std::complex)); + } + } } if (dbg) { double dh = 0.0; @@ -299,6 +352,20 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { df.write(reinterpret_cast(&drho_in_g[ig]), sizeof(std::complex)); } + static bool dumped_g = false; + if (!dumped_g) { + dumped_g = true; + std::ofstream gf("/tmp/opencode/drho_iters/gcar.bin", + std::ios::binary); + for (int ig = 0; ig < pw_rho_->npw; ++ig) { + const double gx = pw_rho_->gcar[ig].x; + const double gy = pw_rho_->gcar[ig].y; + const double gz = pw_rho_->gcar[ig].z; + gf.write(reinterpret_cast(&gx), sizeof(double)); + gf.write(reinterpret_cast(&gy), sizeof(double)); + gf.write(reinterpret_cast(&gz), sizeof(double)); + } + } } } } @@ -363,9 +430,16 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } 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]); + // b = -(dV_ext + dV_sc)|psi_n>; the homogeneous probe keeps + // only the screening part to isolate the linear map M + if (jprobe) { + for (size_t i = 0; i < rhs.size(); ++i) { + rhs[i] = -dv_sc[ib][i]; + } + } else { + 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; @@ -390,9 +464,31 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { // ---- 3. first-order density and mixing rho_.compute_drho(gs_psi_, wg_, q_idx, data_); + if (jprobe && iter == 0) { + const std::vector> probe_out = data_.get_drho_g(q_idx, 0); + std::ofstream pf("/tmp/opencode/jprobe_out.bin", std::ios::binary); + for (int ig = 0; ig < pw_rho_->npw; ++ig) { + pf.write(reinterpret_cast(&probe_out[ig]), + sizeof(std::complex)); + } + std::ofstream vf("/tmp/opencode/jprobe_vsc.bin", std::ios::binary); + for (int ir = 0; ir < nrxx; ++ir) { + vf.write(reinterpret_cast(&v_sc_r[ir]), + sizeof(std::complex)); + } + pf.close(); + vf.close(); + std::cout << "JPROBE dumped, exiting" << std::endl; + std::cout.flush(); + std::exit(0); + } rho_.mix_drho(q_idx, data_); residual = rho_.get_residual(q_idx, data_); data_.add_residual(residual); + if (dbg) { + std::cout << "DBG iter=" << iter << " residual=" << residual + << " conv_thr=" << conv_thr_ << std::endl; + } converged = (residual < conv_thr_); } data_.set_converged(converged); @@ -514,8 +610,8 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { std::vector> vh_r(pw_rho_->nrxx); pw_rho_->recip2real(vha_g.data(), vh_r.data()); std::vector> vx_r; - if (xc_ != nullptr) { - std::vector> ar(pw_rho_->nrxx); + if (xc_ != nullptr) { + std::vector> ar(pw_rho_->nrxx); pw_rho_->recip2real(dg_in.data(), ar.data()); xc_->apply(ar, vx_r); } From d83d43523dbb55dcc77476b5b1982817a99e5a67 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 10:25:05 +0800 Subject: [PATCH 16/50] Docs: record DFPT stage-B gap audit and revised execution plan --- .../module_dfpt/PLAN_dfpt_implementation.md | 64 ++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 3b260413056..0af991e5319 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -89,12 +89,47 @@ --- -## B — 数据层收编 - -- `DFPT_IrrepData` 下沉为 `DFPT_PW_Data` 正式数据层(收敛台账、irrep 元数据并入),迁移现有 dfpt 测试。 -- 提交。 - -## A — irrep 分解(最后) +## B — 工程化收编(2026-08-18 修订:先全流程工程验证,对称性后移) + +> 决策记录:INPUT 主文件参数(非 dfpt.in 子文件);irrep 保留接口、优先全流程工程验证; +> 调试插桩暂缓清理(A 阶段验证后统一收尾)。执行序:B1 → B0 → B2 → B3 → B4,每节点 +> 完成 = 代码 + 构建/回归/治理 + git 提交 + 本文档进度回写,再进入下一节点。 + +**B1 — INPUT 参数接线(解除硬编码/死代码)** +- 新增 dfpt 前缀 INPUT 参数:`dfpt_qmesh`(3 int) / `dfpt_qfile`(str,走 `QList::read_from_file`) + / `dfpt_compute_q0`(bool) / `dfpt_loto`(bool) / `dfpt_conv_thr` / `dfpt_max_iter` / `dfpt_mix_beta`。 +- `esolver_dfpt_pw.cpp` 删除硬编码(set_qmesh(1,1,1)/conv_thr/max_iter/空桩 set_parameters), + 改从 `inp` 显式传递(规则 1);`set_compute_q0`/`set_loto` 死代码开关经此激活。 +- `docs/parameters.yaml` + `input-main.md` 同步;`-h dfpt_*` 与 `--check-input` 验证。 + +**B0 — 全流程工程验证(数值验收基线)** +- 真实金刚石 Γ 点(a≈3.567 Å、NC PP、收敛 k 网格;玩具胞 742 cm⁻¹ 仅为 FD 锁定基线): + 声子(ASR + 光学支 vs LDA 文献)、ε∞(各向同性 ≈5.3–5.7)、Z*、LO-TO 方向依赖。 +- 非 Γ q:`dfpt_qmesh` + 公度 k 网格(`build_occ_kq` 公度守卫已备)验证 q≠0 全流程。 +- MPI>1 rank 冒烟(当前打印注释自述 single-rank,未验证面)。 +- `--version`/`-h`/`--check-input` 记录;结论回写本文档。 + +**B2 — ε∞/Z*/LO-TO 输出正式化** +- esolver design-phase std::cout 转正式输出:多 q 布局、LO-TO 修正后频率(每方向)。 +- loto 方向经数据层传递,消除 run() 中 (1,1,1)/√3 硬编码。 + +**B3 — Kerker 型预条件混合** +- `DFPT_Rho` 内自实现 `|G+q|²/(|G+q|²+a²)` 预条件(不引 charge_mixing.h,module_base 无 + 现成 Kerker 已核实);mix_type 支持 plain/kerker。 +- 验收:λ_A1≈−2.2 模型问题 β=0.7 收敛(β 上界 0.62 解除,JPROBE 复用为验收工具); + 金刚石频率与 β 无关(固定点正确性);`dfpt_mix_beta` 默认回调并文档记录。 + +**B4 — 数据层收编** +- 收敛台账(converged_/residuals_/current_iter_ 按 (q,irrep))并入 `DFPT_PW_Data`; + 删除 `DFPT_IrrepData` 适配层与 `get_dpsi_obj` static dummy;测试迁移; + **保留 (q,irrep) 接口形状**(为 irrep 实装留插槽);run() 外层 while 记账语义梳理。 + +**暂缓项(接口保留,后续单独立项)** +- A irrep 分解:LittleGroup 占位(nirr≡1/空 basis)与 run() 3N 回退保持现状。 +- 插桩清理(PTCHK/DYNCHK/MDBG/JPROBE/VKBCHK/DFPT_MIX_BETA):B3 验收仍需 JPROBE。 +- KVectorUtils 薄封装删除:随 A 阶段收尾一并处理。 + +## A — irrep 分解(最后,工程验证完成后立项) - `module_symmetry/little_group.{h,cpp}`:完整不可约表示表 + 投影算子 → 真实 `get_nirr`/`get_mode_basis`(替换占位 =1/空)。 - 测试:金刚石/闪锌矿 Γ/X/L 点 irrep 分解与理论表核对。提交。 @@ -184,5 +219,18 @@ - 后期漂移根因(本轮确诊):残差降至 5e-5 后指数增长(1.27×/iter)、|in| 恒定而 out 偏离 → 垃圾方向与物理分量正交、混合映射本征值 μ=1.2765 恒定(纯本征模);本征模身份 = {200} 壳 6 矢等幅实系数 + {111} 壳 8 矢 ±π/4 相位的 Hermitian 实 A1 呼吸模(seed ~1e-6 舍入级);均匀探针实验(DFPT_JPROBE:注入纯 A1 模 + rhs 去 dV_ext 单迭代直测线性映射)给出 λ_A1 = −2.229(Hartree-only −3.180,XC 削减到 −2.23)——非符号 bug,是最小 G 壳的 Coulomb 刚性(4π/G² 硬核):plain mixing 收敛条件 −2/β+1<λ 要求 β<0.62,物理 T2 模 λ=−1.42(小 G 头部含量少)在 β=0.7 恰好可收敛,故固定点正确而 A1 通道发散;β=0.4 时 μ_A1=−0.29 稳定 - 修复:默认 mix_beta 0.7→0.4(注释记录测得的 |λ|~2.2 与 β 上界 2/(1+|λ_min|),留裕量至 |λ|~5);DFPT_MIX_BETA env 旋钮保留;β=0.4 时 6 位移全部经收敛旗标退出(平均 ~38 iter,总 228),频率/ele 矩阵与 β 无关逐位一致(固定点正确性再验证),收敛 drho manifest 干净(|FD| 比率 0.99994、cos 0.9993、逐点相对差 3.8%);后续正解是 Kerker 型预条件混合(随 B 阶段排期) - ε∞/Z* 打印为空(随 B 阶段);调试插桩(PTCHK/DYNCHK/MDBG/JPROBE dump/VKBCHK/drho dump/DFPT_MIX_BETA env)收尾节点统一清理评审 -- [ ] B 数据层收编 -- [ ] A irrep 分解 +- [ ] B 工程化收编(2026-08-18 修订:B1 INPUT 接线 → B0 全流程验证 → B2 输出正式化 → B3 Kerker → B4 数据层) + - 修订依据的差距盘点(代码 vs 计划交叉核对,2026-08-18): + ① `set_compute_q0`/`set_loto` 全仓库无调用者(死代码,q0/loto 分支不可达); + ② `set_parameters("dfpt.in")` 空桩 + esolver 硬编码 `set_qmesh(1,1,1)`/conv_thr/max_iter(非 Γ q 无法从输入驱动); + ③ ε∞/Z* 打印为 design-phase 临时 std::cout(esolver_dfpt_pw.cpp:322,单 rank 假定); + ④ 混合仍 plain β=0.4(Kerker 预条件排期 B3); + ⑤ DFPT_IrrepData irrep 维度占位穿透、get_dpsi_obj 返回 static dummy; + ⑥ run() 外层 while 形式化(无条件 set_converged(true))、LO-TO 方向硬编码 (1,1,1)/√3; + ⑦ 真实晶格金刚石端到端/非 Γ q/MPI>1 rank 均未冒烟(C7 待办未做)。 + - [ ] B1 INPUT 参数接线 + - [ ] B0 全流程工程验证(真实金刚石 Γ / 非 Γ q / MPI 冒烟) + - [ ] B2 输出正式化 + - [ ] B3 Kerker 预条件混合 + - [ ] B4 数据层收编 +- [ ] A irrep 分解(保留接口,工程验证完成后立项) From 297a2b3a2419ac3a706de8d2049bb0a6a980c6e1 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 11:03:54 +0800 Subject: [PATCH 17/50] Feat: INPUT-driven DFPT parameters (dfpt_qmesh/qfile/compute_q0/loto/conv_thr/max_iter/mix_beta) - read_inp_dfpt.cpp: 7 new INPUT items with checks (loto requires compute_q0) - esolver_dfpt_pw: drop hardcoded qmesh/conv/max_iter and the dfpt.in stub; wire from inp explicitly (rule 1) - DFPT_PW: set_qfile/set_mix_beta/set_compute_q0/set_loto; q file overrides the MP q mesh in init - QList::read_from_file: fill the fallback A1 placeholder irrep (nirr=1) instead of clearing, so the q-file path keeps the 3N displacement fallback - docs/parameters.yaml + input-main.md regenerated (new category) - README example updated --- docs/advanced/input_files/input-main.md | 60 ++++++ docs/parameters.yaml | 56 ++++++ source/source_cell/qlist.cpp | 10 +- source/source_esolver/esolver_dfpt_pw.cpp | 15 +- source/source_io/CMakeLists.txt | 1 + .../module_parameter/input_parameter.h | 9 + .../module_parameter/read_inp_dfpt.cpp | 172 ++++++++++++++++++ .../source_io/module_parameter/read_input.cpp | 1 + .../source_io/module_parameter/read_input.h | 2 + source/source_io/test_serial/CMakeLists.txt | 1 + .../module_dfpt/PLAN_dfpt_implementation.md | 2 +- source/source_pw/module_dfpt/README.md | 8 +- source/source_pw/module_dfpt/dfpt_pw.cpp | 39 +++- source/source_pw/module_dfpt/dfpt_pw.h | 10 +- 14 files changed, 366 insertions(+), 20 deletions(-) create mode 100644 source/source_io/module_parameter/read_inp_dfpt.cpp diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 775af584f41..92967624225 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -584,6 +584,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 @@ -5131,3 +5139,55 @@ - **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) +- **Availability**: *esolver_type = dfpt* +- **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 +- **Availability**: *esolver_type = dfpt* +- **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. + +### dfpt_compute_q0 + +- **Type**: Boolean +- **Availability**: *esolver_type = dfpt* +- **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 +- **Availability**: *esolver_type = dfpt* +- **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 +- **Availability**: *esolver_type = dfpt* +- **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 +- **Availability**: *esolver_type = dfpt* +- **Description**: Set the maximum number of self-consistent DFPT iterations for each atomic displacement. +- **Default**: 100 + +### dfpt_mix_beta + +- **Type**: Real +- **Availability**: *esolver_type = dfpt* +- **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 1ef9c581911..fde26874480 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -2945,6 +2945,62 @@ parameters: default_value: "0" unit: "" availability: "" + - 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: esolver_type = dfpt + - 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: esolver_type = dfpt + - 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: esolver_type = dfpt + - 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: esolver_type = dfpt + - 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: esolver_type = dfpt + - 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: esolver_type = dfpt + - 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: esolver_type = dfpt - name: plot_istate category: Linear Response TDDFT type: Integer diff --git a/source/source_cell/qlist.cpp b/source/source_cell/qlist.cpp index d6169dc3f73..cd13bf1c6ef 100644 --- a/source/source_cell/qlist.cpp +++ b/source/source_cell/qlist.cpp @@ -209,10 +209,12 @@ void QList::read_from_file(const std::string& filename, UnitCell& ucell) { this->normalize_wk(1); } - // no symmetry reduction (no symmetry object in this interface); therefore - // no irrep decomposition either -> clear any stale irrep data - this->nirr_.clear(); - this->irrep_modes_.clear(); + // 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) { diff --git a/source/source_esolver/esolver_dfpt_pw.cpp b/source/source_esolver/esolver_dfpt_pw.cpp index 56f8e0688dd..72652b82c82 100644 --- a/source/source_esolver/esolver_dfpt_pw.cpp +++ b/source/source_esolver/esolver_dfpt_pw.cpp @@ -169,13 +169,16 @@ void ESolver_DFPT_PW::before_all_runners(BaseCell& basecell, const Input_para& i ecutwfc_ = inp.ecutwfc; dft_plus_u_ = inp.dft_plus_u; - // static DFPT configuration; the ground-state data wiring happens in - // init_dfpt after the SCF has converged + // 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_parameters("dfpt.in"); - dfpt_->set_qmesh(1, 1, 1); - dfpt_->set_conv_thr(1e-8); - dfpt_->set_max_iter(100); + 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) diff --git a/source/source_io/CMakeLists.txt b/source/source_io/CMakeLists.txt index b506a2ad794..52d9db494f4 100644 --- a/source/source_io/CMakeLists.txt +++ b/source/source_io/CMakeLists.txt @@ -103,6 +103,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 008414a9c30..8b0f0ce3dfc 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_input.cpp b/source/source_io/module_parameter/read_input.cpp index 30d6b68551e..427cd1b52d4 100644 --- a/source/source_io/module_parameter/read_input.cpp +++ b/source/source_io/module_parameter/read_input.cpp @@ -174,6 +174,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_serial/CMakeLists.txt b/source/source_io/test_serial/CMakeLists.txt index bef63108a32..a5f8374acca 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 diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 0af991e5319..0ac11cd261d 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -226,7 +226,7 @@ ③ ε∞/Z* 打印为 design-phase 临时 std::cout(esolver_dfpt_pw.cpp:322,单 rank 假定); ④ 混合仍 plain β=0.4(Kerker 预条件排期 B3); ⑤ DFPT_IrrepData irrep 维度占位穿透、get_dpsi_obj 返回 static dummy; - ⑥ run() 外层 while 形式化(无条件 set_converged(true))、LO-TO 方向硬编码 (1,1,1)/√3; + ⑥ run() 外层 while 形式化(无条件 set_converged(true)、LO-TO 方向硬编码 (1,1,1)/√3; ⑦ 真实晶格金刚石端到端/非 Γ q/MPI>1 rank 均未冒烟(C7 待办未做)。 - [ ] B1 INPUT 参数接线 - [ ] B0 全流程工程验证(真实金刚石 Γ / 非 Γ q / MPI 冒烟) diff --git a/source/source_pw/module_dfpt/README.md b/source/source_pw/module_dfpt/README.md index 0bfaf97cb74..9b5eae426f1 100644 --- a/source/source_pw/module_dfpt/README.md +++ b/source/source_pw/module_dfpt/README.md @@ -84,9 +84,12 @@ DFPT_PW ModuleDFPT::DFPT_PW dfpt; // dftu is a const Plus_U* wired by the esolver layer ONLY when dft_plus_u // is enabled; pass nullptr otherwise (DFPT never reads PARAM itself). -dfpt.init(ucell, psi, nelec, ecutwfc, dftu); +dfpt.init(ucell, psi, pw_rho, pw_wfc, sf, veff_r, wg, eig, xc, nelec, ecutwfc, dftu); dfpt.set_qmesh(4, 4, 4); dfpt.set_conv_thr(1e-8); +dfpt.set_mix_beta(0.4); +dfpt.set_compute_q0(true); +dfpt.set_loto(true); dfpt.run(); // Get results @@ -94,6 +97,9 @@ std::vector freq = dfpt.get_phonon_freq(q_idx); ModuleBase::matrix eps = dfpt.get_dielectric_tensor(); ``` +The production wiring is INPUT-driven (`esolver_type dfpt` + the `dfpt_*` +parameters in INPUT); see `docs/advanced/input_files/input-main.md`. + ## Development Status - **Phase**: Design phase diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 54d10d97724..64c691e552f 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -72,8 +72,10 @@ class DFPT_PW::Impl { 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; } @@ -111,8 +113,17 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, pimpl_->ecutwfc_ = ecutwfc; pimpl_->dftu_ = dftu; - std::vector mp_grid = {pimpl_->nqx_, pimpl_->nqy_, pimpl_->nqz_}; - pimpl_->qlist_.generate_mesh(ucell, ucell.symm, mp_grid, true); + // 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(); @@ -128,9 +139,9 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, // 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|); 0.4 keeps margin up to |lambda| ~ 5; the - // env knob is a design-phase calibration aid - double mix_beta = 0.4; + // 2 / (1 + |lambda_min|); the INPUT default 0.4 keeps margin up to + // |lambda| ~ 3; the env knob is a design-phase calibration aid + 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) { @@ -729,8 +740,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) { @@ -749,4 +760,18 @@ void DFPT_PW::set_max_iter(int max_iter) { pimpl_->data_.set_max_iter(max_iter); } +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); +} + } // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_pw.h b/source/source_pw/module_dfpt/dfpt_pw.h index b8a5a84e493..33bdc3aa614 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.h +++ b/source/source_pw/module_dfpt/dfpt_pw.h @@ -71,7 +71,8 @@ class DFPT_PW { 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); @@ -79,6 +80,13 @@ class DFPT_PW { 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); + private: class Impl; Impl* pimpl_; From b64ad9f5edc7b7ffde2070f6003f6d70bc40fb6b Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 11:43:30 +0800 Subject: [PATCH 18/50] Test: sync DFPT serial references to production conventions The four serial suites were last green against pre-calibration binaries; three distinct reference gaps surfaced after the full rebuild: - pert/q0 AnalyticDVloc and FD references: the a0047421b phase flip (GS stru_fac convention exp(-i 2pi g.tau), dVloc/dtau = -i (Delta+q)_alpha tpiba Vloc exp(-i 2pi (Delta+q).tau)) was not mirrored in the closed-form references. - rho brute-force G-space and real-space manifests: compute_drho now carries the GS density normalization w/omega (elecstate rhoBandK w1); references divide by omega accordingly. - phon accumulate_electron reference: same phase flip, plus the dynmat mass normalization /sqrt(m_a m_b) (term2) and /m (d2V) that the closed form had silently omitted (fixture mass 12). MODULE_DFPT serial suites 26/26; full regression filter 14/14 (CELL 4 + DFPT 8 + IO 2). Governance: pre-existing exempted include warnings only. --- .../module_dfpt/PLAN_dfpt_implementation.md | 13 ++++++++++- .../test_serial/dfpt_pert_serial_test.cpp | 22 ++++++++++--------- .../test_serial/dfpt_phon_serial_test.cpp | 15 ++++++++----- .../test_serial/dfpt_q0_serial_test.cpp | 7 +++--- .../test_serial/dfpt_rho_serial_test.cpp | 7 ++++-- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 0ac11cd261d..c17d8c3f37e 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -228,7 +228,18 @@ ⑤ DFPT_IrrepData irrep 维度占位穿透、get_dpsi_obj 返回 static dummy; ⑥ run() 外层 while 形式化(无条件 set_converged(true)、LO-TO 方向硬编码 (1,1,1)/√3; ⑦ 真实晶格金刚石端到端/非 Γ q/MPI>1 rank 均未冒烟(C7 待办未做)。 - - [ ] B1 INPUT 参数接线 + - [x] B1 INPUT 参数接线 `297a2b3a2` + - `read_inp_dfpt.cpp` 7 项(qmesh/qfile/compute_q0/loto/conv_thr/max_iter/mix_beta, + check_value:loto 需 compute_q0、qmesh≥1、mix_beta∈(0,1]);CMake 两处接入 + - esolver 删硬编码与 `dfpt.in` 空桩,`set_parameters` 移除,全走显式 setter(规则 1); + `DFPT_PW` 新增 set_qfile/set_mix_beta/set_compute_q0/set_loto(q 文件优先于 MP 网格) + - `QList::read_from_file` 改填占位 A1 irrep(nirr=1)而非清空——q 文件路径下 run() + 依赖 get_nirr≥1 才进 3N 回退求解 + - 修复(测试捕获):空默认串参数回写 INPUT 再读回时 `str_values[0]` 越界 → dfpt_qfile + 采用 pseudo_dir 的 `get_size()==0` 守卫模式 + - docs/parameters.yaml 新类目 + input-main.md 经 docs/generate_input_main.py 再生; + `-h dfpt_qmesh/dfpt_mix_beta` 验证;回归 14/14(CELL 4 + DFPT 8 + IO 2); + 治理仅既有豁免 WARNING - [ ] B0 全流程工程验证(真实金刚石 Γ / 非 Γ q / MPI 冒烟) - [ ] B2 输出正式化 - [ ] B3 Kerker 预条件混合 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 index 1187998fb14..d18bdc23e08 100644 --- 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 @@ -243,7 +243,9 @@ class DFPTPertSerialTest : public testing::Test return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega / g2_bohr; } - // analytic dVloc/dtau_alpha coefficient at displacement vector w (1/lat0) + // 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 { const double w2 = w * w; @@ -251,8 +253,8 @@ class DFPTPertSerialTest : public testing::Test { 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) + 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)); } }; @@ -291,10 +293,10 @@ TEST_F(DFPTPertSerialTest, DVlocDtauMatchesFiniteDifference) 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 - const double ap = ModuleBase::TWO_PI * (w * (tau_ + eps * d)); - const double am = ModuleBase::TWO_PI * (w * (tau_ - eps * d)); + // 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_); @@ -471,14 +473,14 @@ TEST_F(DFPTPertSerialTest, BuildVkbL0MatchesIndependentSimpson) 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 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} + // 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; @@ -492,7 +494,7 @@ TEST_F(DFPTPertSerialTest, BuildVkbL0MatchesIndependentSimpson) { continue; } - const double arg = ModuleBase::TWO_PI * (gk[ig] * dtau); + 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); 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 index 6615bfab66a..5fb597c0689 100644 --- 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 @@ -468,8 +468,9 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) // expected: row 1 (atom 0, dir 1). The RHS on the k+q basis vector igl // carries the momentum w = Delta + q (Delta = G'' since k + q = 0 makes // every k+q basis vector a pure reciprocal-lattice harmonic G''): - // RHS^a(G'') = i tpiba w_a Vloc(w^2) e^{i 2pi w.tau} (psi is a single - // G'=0 plane wave and the Coulomb potential has no nonlocal part). + // RHS^a(G'') = -i tpiba w_a Vloc(w^2) e^{-i 2pi w.tau} (psi is a single + // G'=0 plane wave and the Coulomb potential has no nonlocal part); the + // GS structure-factor phase convention is exp(-i 2pi g.tau). ModuleDFPT::DFPT_KQ_Basis kq; kq.init(&pw_wfc_, q_cart_, 0); for (int adir = 0; adir < 3; ++adir) @@ -484,8 +485,8 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) { continue; // Delta + q = 0 component dropped by dVloc } - const double arg = ModuleBase::TWO_PI * (w * tau_); - const std::complex rhs = std::complex(0.0, 1.0) + const double arg = -ModuleBase::TWO_PI * (w * tau_); + const std::complex rhs = std::complex(0.0, -1.0) * (ucell_.tpiba * w[adir]) * VlocCoulomb(w2 * ucell_.tpiba2) * std::complex(std::cos(arg), std::sin(arg)); @@ -497,13 +498,15 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) // |u|^2 = 1 keeps only Delta=0: w = q; the production path // accumulates the d2V expectation with the occupation weight const double w2 = q_cart_ * q_cart_; - const double arg = ModuleBase::TWO_PI * (q_cart_ * tau_); + const double arg = -ModuleBase::TWO_PI * (q_cart_ * tau_); expect_d2 = -(ucell_.tpiba * q_cart_[1]) * (ucell_.tpiba * q_cart_[adir]) * VlocCoulomb(w2 * ucell_.tpiba2) * std::complex(std::cos(arg), std::sin(arg)) * wg(0, 0); } - const std::complex expect = 2.0 * wg(0, 0) * expect_cross + expect_d2; + // accumulate divides term2 by sqrt(m_a m_b) and d2V by m (m = 12 here) + const std::complex expect + = (2.0 * wg(0, 0) * expect_cross + expect_d2) / ucell_.atoms[0].mass; // note: accumulate uses (da=adir for the column, db=1 for the row); // d2vloc_r multiplies w_da w_db symmetrically, so the closed form // above (dir1 x adir) matches either ordering 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 index 59972aa30fb..ebcffe7258e 100644 --- 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 @@ -251,7 +251,8 @@ class DFPTQ0SerialTest : public testing::Test / g2_bohr; } - // analytic dVloc/dtau_dir coefficient at displacement vector w (1/lat0) + // analytic dVloc/dtau_dir coefficient at displacement vector w (1/lat0), + // GS structure-factor phase convention exp(-i 2pi w.tau) std::complex AnalyticDVloc(int dir, const ModuleBase::Vector3& w) const { const double w2 = w * w; @@ -259,8 +260,8 @@ class DFPTQ0SerialTest : public testing::Test { 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]) + 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_rho_serial_test.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_rho_serial_test.cpp index 92d8c01096d..2bfd45cc156 100644 --- 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 @@ -170,7 +170,8 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoMatchesBruteForceGSpace) 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 = sum_G c*_G d_{G+Delta}, brute-forced over the two lists + // A_Delta = (w / omega) * sum_G c*_G d_{G+Delta} with the GS density + // normalization (elecstate rhoBandK w1), brute-forced over the lists std::complex aref(0.0, 0.0); for (int jgl = 0; jgl < kq.get_npwk(); ++jgl) { @@ -186,6 +187,7 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoMatchesBruteForceGSpace) } } } + aref *= wg(0, 0) / pw_rho_.omega; err2 += std::norm(drho_g[ig] - aref); ref2 += std::norm(aref); } @@ -251,7 +253,8 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoRealSpaceMatchesDirectSum) } 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)); - const double ref = 2.0 * (std::conj(u) * du * eq).real(); + // same GS normalization (w / omega) as the stored manifest density + const double ref = 2.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); } From dc82fac9b0c45ed2da654cc34c04273d7d9d883f Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 20:59:20 +0800 Subject: [PATCH 19/50] Fix: multi-k DFPT ball-label matching and smeared-occupation projector cliff Two independent defects broke DFPT responses whenever the ground-state k list held more than one inequivalent point (nk > 1): 1. build_occ_kq assumed the k+q and k(q) balls share FFT-cell G labels. When k+q folds onto a different label of the same physical point (e.g. lists holding both (1/2,0,0) and (-1/2,0,0)), the projected states became garbage and the Sternheimer solve diverged. Balls are now matched through reciprocal-lattice integer triples f + dn = f', with dn = k(ik)+q-k(ikq); the ikq-side labels are read through PW_Basis_K::getgcar because collect_local_pw(erf) rebuilds gcar into a per-k ball layout [ik*npwk_max+igl], destroying the parent global-ig layout the old code indexed. 2. The absolute wg < 1e-8 occupied-band cliff made the Sternheimer projector jump between k samplings: a smeared Fermi-tail band with weight ~1e-6 sits on either side of the threshold depending on the sampling's Fermi level, opening or closing its empty-state channel in (H-eps)^-1 and shifting converged force constants by ~10%. A shared dfpt_band_occupied() now classifies a band as occupied iff wg(ik,ib) > 0.5*wg(ik,0) (majority occupation), applied consistently in the projector build, the solve driver, the response density, the 2n+1 assembly and the q0 valence/conduction split. Diamond-Si 2-atom validation against finite differences (sym=0): - single Gamma: D00 0.0208553 vs FD 0.020854 (unchanged) - single L: D00 0.0129282 vs FD 0.012927 (new FD reference) - {L,-L}: equals single-L exactly (was divergent), ASR row sums ~1e-6 - {Gamma,L}: D00 0.0166416 vs FD 0.016642 (was 0.0182462, +9.6%) - {L,X} and weight-skewed {G,L} variants consistent; 14/14 MODULE_DFPT/CELL/IO serial regressions pass. --- source/source_pw/module_dfpt/dfpt_phon.cpp | 132 ++++++++++-- source/source_pw/module_dfpt/dfpt_pw.cpp | 202 +++++++++++++++--- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 48 +++++ source/source_pw/module_dfpt/dfpt_pw_data.h | 41 ++++ source/source_pw/module_dfpt/dfpt_q0.cpp | 6 +- source/source_pw/module_dfpt/dfpt_rho.cpp | 2 +- 6 files changed, 374 insertions(+), 57 deletions(-) diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index 668ec13aaa3..8c24fb4f023 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -329,12 +329,18 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int 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) - std::vector>>> dpsib(nk); - for (int ik = 0; ik < nk; ++ik) { - dpsib[ik].resize(nbands); - for (int ib = 0; ib < nbands; ++ib) { - dpsib[ik][ib] = data.get_dpsi(q_idx, ik, ib); + // 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); + } } } @@ -354,17 +360,23 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, for (int idir = 0; idir < 3; ++idir) { const int cola = 3 * iat + idir; // ---- term 2 over all k,n ---- - // complex accumulation: at a generic q the single-k matrix - // elements are complex (the imaginary parts pair-conjugate over - // the k star), and the Hermitian symmetrization in assemble - // relies on them. + // 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); const bool dbg2 = (getenv("DFPT_DEBUG") != nullptr); + const bool xbk = (getenv("DFPT_XB") != nullptr && rowb == 0 + && (cola == 0 || cola == 3 || cola == 1)); + std::complex cross(0.0, 0.0); + std::vector> cross_k; for (int ik = 0; ik < nk; ++ik) { pert_->apply_dv(q_idx, ik, psi, data); + std::complex cross_k_sum(0.0, 0.0); for (int ib = 0; ib < nbands; ++ib) { - if (wg(ik, ib) < 1.0e-8) { + if (!dfpt_band_occupied(wg, ik, ib)) { continue; } const std::vector> rhs = data.get_dpsi(q_idx, ik, ib); @@ -373,18 +385,40 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, continue; } std::complex dot(0.0, 0.0); + double nsol = 0.0; + double nrhs = 0.0; for (size_t i = 0; i < sol.size(); ++i) { dot += std::conj(sol[i]) * rhs[i]; + nsol += std::norm(sol[i]); + nrhs += std::norm(rhs[i]); } cross += wg(ik, ib) * dot; + cross_k_sum += wg(ik, ib) * dot; + if (xbk) { + std::cout << "XB rowb=" << rowb << " cola=" << cola + << " ik=" << ik << " ib=" << ib + << " w=" << wg(ik, ib) + << " dot=(" << dot.real() << "," << dot.imag() << ")" + << " |sol|=" << std::sqrt(nsol) + << " |rhs|=" << std::sqrt(nrhs) + << std::endl; + } } + cross_k.push_back(cross_k_sum); } - dynmat_accum_(rowb, cola) += 2.0 * cross - / std::sqrt(ucell_->atoms[ucell_->iat2it[atom_idx]].mass + 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; if (dbg2) { std::cout << "DYNCHK term2 rowb=" << rowb << " cola=" << cola - << " 2cross=" << 2.0 * cross.real() << std::endl; + << " cross=" << cross.real() + << " imag=" << cross.imag(); + for (size_t ikp = 0; ikp < cross_k.size(); ++ikp) { + std::cout << " k" << ikp << "=" << cross_k[ikp].real(); + } + std::cout << std::endl; } // ---- same-atom anharmonic term ---- @@ -408,7 +442,7 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, kq.init(pert_->get_pw_wfc(), q_cart, ik); const int npwk_kq = kq.get_npwk(); for (int ib = 0; ib < nbands; ++ib) { - if (wg(ik, ib) < 1.0e-8) { + if (!dfpt_band_occupied(wg, ik, ib)) { continue; } pert_->get_pw_wfc()->recip2real(&psi(ik, ib, 0), u_r.data(), ik); @@ -445,15 +479,24 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, d2sum += wg(ik, ib) * expect / static_cast(pw_rho_->nxyz); d2sum_loc += wg(ik, ib) * expect_loc / static_cast(pw_rho_->nxyz); d2sum_nl += wg(ik, ib) * expect_nl / static_cast(pw_rho_->nxyz); + if (dbg2 && ib == 0) { + std::cout << "DYNCHK d2k rowb=" << rowb << " cola=" << cola + << " ik=" << ik << " acc=" << d2sum.real() << std::endl; + } } } - dynmat_accum_(rowb, cola) += d2sum - / ucell_->atoms[ucell_->iat2it[atom_idx]].mass; + 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; + } if (dbg2) { std::cout << "DYNCHK d2 rowb=" << rowb << " cola=" << cola << " d2sum=" << d2sum.real() << " loc=" << d2sum_loc.real() - << " nl=" << d2sum_nl.real() << std::endl; + << " nl=" << d2sum_nl.real() + << " imag=" << d2sum.imag() << std::endl; } } } @@ -501,6 +544,14 @@ void DFPT_Phon::assemble(int q_idx, DFPT_PW_Data& data) { } std::cout << std::endl; } + std::cout << "DYNCHK electronic accum matrix (imag):" << std::endl; + for (int i = 0; i < nat3; ++i) { + std::cout << "DYNCHK elei row " << i << ":"; + for (int j = 0; j < nat3; ++j) { + std::cout << " " << dynmat_accum_(i, j).imag(); + } + std::cout << std::endl; + } } for (int i = 0; i < nat3; ++i) { for (int j = 0; j < nat3; ++j) { @@ -542,9 +593,50 @@ void DFPT_Phon::diagonalize(int q_idx, DFPT_PW_Data& data) { int info = 0; LapackConnector::zheev('N', 'U', nat3, dyn, nat3, w.data(), work.data(), -1, rwork.data(), &info); + if (getenv("DFPT_DEBUG") != nullptr) { + std::vector> auxp(nat3 * nat3); + for (int i = 0; i < nat3; ++i) { + for (int j = 0; j < nat3; ++j) { + auxp[i * nat3 + j] = dyn(j, i); + } + } + std::cout << "DYNCHK4 pre-call dyn (logical rows):" << std::endl; + for (int i = 0; i < nat3; ++i) { + std::cout << "DYNCHK4 row " << i << ":"; + for (int j = 0; j < nat3; ++j) { + std::cout << " " << dyn(i, j); + } + std::cout << std::endl; + } + std::vector w2(nat3, 0.0); + std::vector rwork2(std::max(1, 3 * nat3 - 2), 0.0); + std::vector> wq(1); + int infoq = 0; + zheev_("N", "U", &nat3, auxp.data(), &nat3, w2.data(), wq.data(), + new int(-1), rwork2.data(), &infoq); + const int lwork2 = static_cast(wq[0].real()); + std::cout << "DYNCHK4 query lwork=" << lwork2 << " infoq=" << infoq << std::endl; + std::vector> work2(std::max(1, lwork2)); + int info2 = 0; + zheev_("N", "U", &nat3, auxp.data(), &nat3, w2.data(), work2.data(), + new int(std::max(1, lwork2)), rwork2.data(), &info2); + std::cout << "DYNCHK4 direct zheev_ info=" << info2 << " eig:"; + for (int i = 0; i < nat3; ++i) { + std::cout << " " << w2[i]; + } + std::cout << std::endl; + } 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); + static_cast(work.size()), rwork.data(), &info); + if (getenv("DFPT_DEBUG") != nullptr) { + std::cout << "DYNCHK4 connector w info=" << info << " workopt=" << work.size() + << " eig:"; + for (int i = 0; i < nat3; ++i) { + std::cout << " " << w[i]; + } + std::cout << std::endl; + } // 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) diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 64c691e552f..2e5f4268ad6 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include @@ -202,39 +204,144 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { kq.init(pw_wfc_, q_cart, ik); const int npw_kq = kq.get_npwk(); - // reverse map FFT cell -> per-k G index at ikq (the two k balls are - // enumerated per k, only the cell position identifies the G) - std::vector jgl_of_cell(pw_wfc_->nxyz, -1); + // 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 int isz = pw_wfc_->getigl2isz(ikq, jgl); - 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; - jgl_of_cell[(ix * pw_wfc_->ny + iy) * pw_wfc_->nz + iz] = 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(); + int dbg_miss = 0; + int dbg_tot = 0; for (int m = 0; m < nbands; ++m) { - if (wg_(ikq, m) < 1.0e-8) { + 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 int isz = kq.get_ig2isz(igl); - 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; - const int jgl = jgl_of_cell[(ix * pw_wfc_->ny + iy) * pw_wfc_->nz + iz]; - if (jgl >= 0) { - state[igl] = gs_psi_(ikq, m, jgl); + 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); + } else { + ++dbg_miss; } + ++dbg_tot; } occ_kq_[ik].push_back(std::move(state)); } + if (getenv("DFPT_DEBUG") != nullptr) { + std::cout << "OCCCHK ik=" << ik << " ikq=" << ikq + << " dn=(" << dn_i[0] << "," << dn_i[1] << "," << dn_i[2] << ")" + << " npw_kq=" << npw_kq + << " npwk_ikq=" << pw_wfc_->npwk[ikq] + << " miss=" << dbg_miss << "/" << dbg_tot << std::endl; + if (dbg_miss > 0 && npw_kq > 0) { + std::set> kq_labels; + for (int igl = 0; igl < npw_kq; ++igl) { + const ModuleBase::Vector3 gf = kq.get_gcar(igl) * ginv; + kq_labels.insert({static_cast(std::round(gf.x)), + static_cast(std::round(gf.y)), + static_cast(std::round(gf.z))}); + } + std::set> gs_labels; + std::set gs_igs; + int ig_max = -1; + for (int jgl = 0; jgl < pw_wfc_->npwk[ikq]; ++jgl) { + const int ig = pw_wfc_->getigl2ig(ikq, jgl); + gs_igs.insert(ig); + if (ig > ig_max) { + ig_max = ig; + } + const ModuleBase::Vector3 gf + = pw_wfc_->getgcar(ikq, jgl) * ginv; + gs_labels.insert({static_cast(std::round(gf.x)), + static_cast(std::round(gf.y)), + static_cast(std::round(gf.z))}); + } + std::cout << "OCCCHK gs_unique_ig=" << gs_igs.size() + << "/" << pw_wfc_->npwk[ikq] + << " ig_max=" << ig_max + << " npw=" << pw_wfc_->npw + << " npwk_max=" << pw_wfc_->npwk_max + << std::endl; + int only_kq = 0; + std::cout << "OCCCHK labels kq=" << kq_labels.size() + << " gs=" << gs_labels.size(); + for (const auto& k : kq_labels) { + if (gs_labels.count(k) == 0) { + ++only_kq; + } + } + std::cout << " only_kq=" << only_kq << std::endl; + int shown = 0; + for (const auto& k : kq_labels) { + if (gs_labels.count(k) == 0) { + std::cout << "OCCCHK kq-only (" << k[0] << "," << k[1] << "," + << k[2] << ")"; + if (++shown >= 6) { + break; + } + } + } + if (shown > 0) { + std::cout << std::endl; + } + shown = 0; + for (const auto& k : gs_labels) { + if (kq_labels.count(k) == 0) { + std::cout << "OCCCHK gs-only (" << k[0] << "," << k[1] << "," + << k[2] << ")"; + if (++shown >= 6) { + break; + } + } + } + if (shown > 0) { + std::cout << std::endl; + } + const ModuleBase::Vector3 gf0 = kq.get_gcar(0) * ginv; + std::cout << "OCCCHK kq key0 gf=(" << gf0.x << "," << gf0.y << "," + << gf0.z << ")"; + const ModuleBase::Vector3 gj0 + = pw_wfc_->getgcar(ikq, 0) * ginv; + std::cout << " ikq gf0=(" << gj0.x << "," << gj0.y << "," << gj0.z << ")" + << std::endl; + } + } } last_q_ = q_idx; last_ik_ = -1; @@ -269,6 +376,9 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { 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) { data_.set_current_iter(iter); @@ -380,6 +490,7 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } } } + v_sc_r_last = v_sc_r; // ---- 2. Sternheimer solve of every occupied (k, band) for (int ik = 0; ik < nk; ++ik) { @@ -426,7 +537,7 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } } for (int ib = 0; ib < nbands; ++ib) { - if (wg_(ik, ib) < 1.0e-8) { + if (!dfpt_band_occupied(wg_, ik, ib)) { continue; // unoccupied: no Sternheimer equation } std::vector> rhs = data_.get_dpsi(q_idx, ik, ib); @@ -503,6 +614,20 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { converged = (residual < conv_thr_); } data_.set_converged(converged); + // 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); + } // design-phase validation: dump converged self-consistent drho on the // shared real-space grid for direct comparison with finite differences @@ -523,19 +648,21 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { if (dbg && q_idx == 0 && iat == 0 && idir == 0 && nk > 0 && nbands > 4) { // gauge check: must vanish for occupied k (Sternheimer // gauge); a nonzero admixture pollutes term2 via occ-occ dV elements - for (int n = 0; n < 4; ++n) { - const std::vector>& dps = data_.get_dpsi(q_idx, 0, n); - const int npwg = gs_psi_.get_nbasis(); - if (static_cast(dps.size()) != npwg) { - continue; - } - for (int k = 0; k < 4; ++k) { - std::complex dot(0.0, 0.0); - for (int ig = 0; ig < npwg; ++ig) { - dot += std::conj(gs_psi_(0, k, ig)) * dps[ig]; + for (int ikg = 0; ikg < nk; ++ikg) { + for (int n = 0; n < 4; ++n) { + const std::vector>& dps = data_.get_dpsi(q_idx, ikg, n); + const int npwg = gs_psi_.get_nbasis(); + if (static_cast(dps.size()) != npwg) { + continue; + } + for (int k = 0; k < 4; ++k) { + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npwg; ++ig) { + dot += std::conj(gs_psi_(ikg, k, ig)) * dps[ig]; + } + std::cout << "PTCHK gauge ik=" << ikg << " n=" << n << " k=" << k + << " =" << dot << std::endl; } - std::cout << "PTCHK gauge n=" << n << " k=" << k - << " =" << dot << std::endl; } } // stash the solved dpsi (apply_dv below reuses the slots) @@ -695,11 +822,20 @@ void DFPT_PW::run() { && irrep_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_, diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index 8b9213dfb5c..18c48fbba90 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -53,6 +53,54 @@ void DFPT_PW_Data::set_docc(int q_idx, const std::vector>& 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>>>(); +} + 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]; diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index 80d24e5b78b..0507c95b37a 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -21,6 +21,23 @@ class Plus_U; 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(); @@ -122,6 +139,24 @@ class DFPT_PW_Data { /// 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; private: ModuleCell::QList* qlist_ = nullptr; @@ -161,6 +196,12 @@ class DFPT_PW_Data { /// DFT+U reservation state (U0) const Plus_U* 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_; int max_iter_ = 100; double conv_thr_ = 1e-8; diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index 0255adcece6..2699bc9ed70 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -193,11 +193,11 @@ void DFPT_Q0::compute_eps(const psi::Psi>& psi, double chi = 0.0; for (int ik = 0; ik < nk; ++ik) { for (int v = 0; v < nbands; ++v) { - if (wg(ik, v) < 1.0e-8) { + if (!dfpt_band_occupied(wg, ik, v)) { continue; // empty } for (int c = 0; c < nbands; ++c) { - if (wg(ik, c) >= 1.0e-8) { + if (dfpt_band_occupied(wg, ik, c)) { continue; // occupied } const double de = eig(ik, c) - eig(ik, v); @@ -247,7 +247,7 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, for (int ik = 0; ik < nk; ++ik) { pert_->apply_dv(0, ik, psi, data); for (int v = 0; v < nbands; ++v) { - if (wg(ik, v) < 1.0e-8) { + if (!dfpt_band_occupied(wg, ik, v)) { continue; // empty } const std::vector> rhs = diff --git a/source/source_pw/module_dfpt/dfpt_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index e01bb106282..edc4830f8c9 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -98,7 +98,7 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, } for (int ib = 0; ib < nbands; ++ib) { const double w = wg(ik, ib); - if (w < 1.0e-8) { + 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) From 276a44434b65d706ffdd2354fa1f1ec0c4a6594c Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 20:59:53 +0800 Subject: [PATCH 20/50] Docs: record multi-k DFPT root causes and FD validation matrix in PLAN --- .../module_dfpt/PLAN_dfpt_implementation.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index c17d8c3f37e..e4bb7d67514 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -241,6 +241,32 @@ `-h dfpt_qmesh/dfpt_mix_beta` 验证;回归 14/14(CELL 4 + DFPT 8 + IO 2); 治理仅既有豁免 WARNING - [ ] B0 全流程工程验证(真实金刚石 Γ / 非 Γ q / MPI 冒烟) + - [x] 多 k(nk>1)数值错误根因与修复 `(本轮,6 文件 +374/−57)` + - 排除法完成:sym=0/1、v_sc 假设、LAPACK/BLAS、npw 不匹配、球大小不匹配({L 392, X + 388} 完全正确)、δρ-cube FD 路线(k 采样噪声地板 >> 位移信号,判死)、d2/计算 + 通道(d2 混合=孤立预测精确一致) + - **根因 1(标签折叠)**:build_occ_kq 假设 k+q 球与 k(ikq) 球共享同 FFT 胞 G 标签; + {L,−L} 时 −L 折叠到 L 标签(差 b1),标签错位 → 投影态垃圾 → 核检查失败+发散。 + 修复 = 倒格矢整数三元组匹配 f+dn=f'(dn=k(ik)+q−k(ikq)),ikq 侧标签经 + `PW_Basis_K::getgcar` 读取——关键发现:`collect_local_pw(erf)` 把 gcar 重建为 + per-k 球布局 [ik*npwk_max+igl],父类全局 ig 布局已毁(nk=1 曾靠堆残留"幸运"通过) + - **根因 2(smearing 投影悬崖)**:wg<1e-8 绝对阈使投影器随 k 采样跳变——{Γ,L} 采样 + 的 E_f 使 L 带占据带尾 w=5e-6 跨过阈值,进入 P_c 投影 → 其空态通道在 (H−ε)⁻¹ + 中关闭 → 收敛响应差 ~10%(X03 +46%、ASR 行和违反 21%)。权重 1% Γ 实验证实损伤 + 与 w_Γ 无关(结构性);β=0 单迭代实验证实同 rhs/本征值下 |dψ| 差 8%(纯投影效应)。 + 修复 = 共享 `dfpt_band_occupied()`:wg(ik,ib) > 0.5·wg(ik,0)(多数占据判据), + 一致应用于投影器/求解驱动/drho/2n+1 装配/q0 v-c 划分 + - FD 验证矩阵(sym=0 金刚石 2 原子,FD 模板 b0_si_k050_fd2/run_fd.py 派生): + 单 Γ D00 0.0208553 vs FD 0.020854;单 L 0.0129282 vs FD 0.012927(**新增 FD + 基准** b0_si_kLL1_fd);{L,−L} = 单 L 逐位一致(原发散);{Γ,L} 0.0166416 vs + FD 0.016642(原 0.0182462,+9.6%);{L,X}(两不等价非 Γ 点)与权重偏斜 + {Γ,L} 变体全部自洽;ASR 行和全部 ~1e-6 + - 调试方法学沉淀:XB per-(ik,ib) 分解仅 iter-1(v_sc=0)可比但受 GS 采样差异混淆; + ASR 行和 = 免 FD 的在跑检测器;跨采样对比仅 {L}vs{L,−L}(BZ 等价)合法 + - 回归 14/14(CELL 4 + DFPT 8 + IO 2);治理仅既有 header/docs WARNING + - 遗留:调试插桩(OCCCHK miss/集合计数、PTCHK/DYNCHK2/4/XB/MDBG/JPROBE)保留至 + B0 收尾统一清理(用户决策);真正的金属分数占据 DFPT(de Gironcoli 成对方程) + 超出当前绝缘体范围,dfpt_metal 占位 - [ ] B2 输出正式化 - [ ] B3 Kerker 预条件混合 - [ ] B4 数据层收编 From 2cf780de8da96286766fd600f8b6b0bc9637abbb Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 21:58:05 +0800 Subject: [PATCH 21/50] Fix: reject metallic smearing occupations in DFPT with an explicit guard An unshifted 2x2x2 mesh of diamond Si with the default gauss sigma 0.015 Ry places the smearing Fermi level 1.3 sigma below the Gamma VBM (band occupations 0.92), and finite differences of the same ground state then give force constants ~2.8x softer than DFPT: the E_f response (d mu / d tau channel) is included automatically in any finite-difference ground state but has no counterpart in the Sternheimer flow (DFPT_Metal is a design-phase stub, C4). Without a guard the run converges cleanly and reports silently wrong numbers. DFPT_PW::init now scans the final wg and quits with an explicit message when any band sits measurably between 0 and its full reference (relative weight in (1e-3, 1-1e-3)); negligible gauss tails are tolerated as the insulator limit. Validation matrix for the regime boundary (diamond Si 2x2x2, sym=0): - sigma 0.015: Gamma VBM 92% occupied -> guard fires (was 2.8x off FD) - sigma 0.007: VBM 99.92% occupied -> guard passes, 3.8% off FD (residual dmu channel scales with tail weight) - sigma 0.005: VBM 99.9996% occupied -> 0.05% off FD (insulator limit; D00 0.0127458 vs FD 0.012739), off-diagonals and ASR exact Also validated in this round: single k=0.25,0,0 (D row0 real parts match FD to 6e-7; imaginary antisymmetric parts are the expected one-sided-k Hermitian artifact, the physical force constants are the real parts), and single k=0.5,0,0 with symmetry=0 now reproduces the L-point reference bitwise (symmetry=1 changes the single-k ground state itself and is out of scope for FD comparison). 14/14 MODULE_DFPT/CELL/IO serial regressions pass. MPI>1 smoke (-np 2) aborts with MPI_ERR_TRUNCATE in the DFPT phase: distributed layouts are not yet supported and fail loudly. --- source/source_pw/module_dfpt/dfpt_pw.cpp | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 2e5f4268ad6..ff7b3a4230b 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -20,6 +20,7 @@ #include "source_base/constants.h" #include #include "source_base/global_function.h" +#include #include "source_cell/qlist.h" #include "source_pw/module_pwdft/stru_fac.h" @@ -110,6 +111,32 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, 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; From 6dbb87209d1313ba78fa485cbe0e54cab1d281cc Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 21:58:47 +0800 Subject: [PATCH 22/50] Docs: record validation-ladder extension, metallic-regime boundary, MPI smoke in PLAN --- .../module_dfpt/PLAN_dfpt_implementation.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index e4bb7d67514..1188612ba9c 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -267,6 +267,29 @@ - 遗留:调试插桩(OCCCHK miss/集合计数、PTCHK/DYNCHK2/4/XB/MDBG/JPROBE)保留至 B0 收尾统一清理(用户决策);真正的金属分数占据 DFPT(de Gironcoli 成对方程) 超出当前绝缘体范围,dfpt_metal 占位 + - [x] 验证梯队扩展(单 k 0.25 / k 网格 / 金属占据区界 / ε∞Z* / MPI 冒烟)`(本轮)` + - **新增 FD 通过项**:单 k=(0.25,0,0)(Λ 点,392 球)D 行 0 实部 vs FD 全部 ~6e-7 + (0.0104952/0.00304483/−0.0104942 vs 0.010495/0.003044/−0.010495);单边 k 采样的 + Hermitian 虚部(D(0,3) imag 0.0061)= X_ba≠X_ab 的预期产物,物理力常数取实部 + (FD 证实);2×2×2 非移位网格(σ=0.005 绝缘区)D00 0.0127458 vs FD 0.012739 + (0.05%),非对角/ASR 精确 + - **金属占据区界定量**(2×2×2 网格,Γ VBM 带尾):σ=0.015 → VBM 92% 占据,FD 比 + DFPT 软 2.83×(FD 含 dμ/dτ 响应、Sternheimer 流无此通道);σ=0.007 → 99.92%, + 差 3.8%;σ=0.005 → 99.9996%,差 0.05%——残差随带尾权重缩小,dμ 通道缺失的干净 + 指纹。**守卫**:DFPT_PW::init 扫描最终 wg,任一带相对占据落入 (1e-3, 1−1e-3) 即 + WARNING_QUIT(显式拒绝而非静默错值,与 C4 哲学一致;k222 σ=0.015 实测触发) + - **symmetry=1 单 k 陷阱**:k050(0.5,0,0) sym=1 与 sym=0 GS 本征值差 2.6e-3 Ry + (−0.234996 vs −0.237553)→ D 差 0.3-4.4%;sym=0 重跑 = kLL1 逐位一致。FD 基准 + 全部 sym=0,跨 sym 比较非法 + - ε∞/Z*:打印链路通;单 Γ 值(105/55.9)= 长度规范简并分母伪影;8-k 值 + (4.75/6.68)= 采样受限;真验证需密网格(8×8×8 ~10h 串行,推迟为过夜项) + - MPI 冒烟(-np 2,kG):DFPT 相位 MPI_ERR_TRUNCATE 硬崩溃(分布式布局未支持, + 响亮失败无静默错值);Stern CG 标量积无 Allreduce 等串行假设已知,MPI 支持另立 + 工作项 + - 案例清单(/tmp/opencode/):k025/k050(已 sym=0 修正)、k222s007/k222s005(+ + _fd 配对)、gamma_k222_nosym(守卫触发样本)、kG_mpi2(崩溃样本) + - [ ] B0 残余:非 Γ q 文件路径冒烟(dfpt_qfile + QList 端到端从未运行);8×8×8 过夜验证; + 插桩清理评审 - [ ] B2 输出正式化 - [ ] B3 Kerker 预条件混合 - [ ] B4 数据层收编 From 346ab2fd31880d0d7b641fdd1eca7b92164738d6 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 18 Aug 2026 22:02:42 +0800 Subject: [PATCH 23/50] Docs: record non-Gamma q smoke results (dfpt_qfile end-to-end, q<->-q consistency) --- .../module_dfpt/PLAN_dfpt_implementation.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 1188612ba9c..fbcbdd7931a 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -288,8 +288,18 @@ 工作项 - 案例清单(/tmp/opencode/):k025/k050(已 sym=0 修正)、k222s007/k222s005(+ _fd 配对)、gamma_k222_nosym(守卫触发样本)、kG_mpi2(崩溃样本) - - [ ] B0 残余:非 Γ q 文件路径冒烟(dfpt_qfile + QList 端到端从未运行);8×8×8 过夜验证; - 插桩清理评审 + - [ ] B0 残余:8×8×8 过夜验证(ε∞/Z* 与 D 的密网格收敛);非 Γ q 的物理级验证 + (超胞 FD 或色散对照);插桩清理评审 + - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` + - dfpt_qfile + QList::read_from_file 端到端首次运行:q=(0.5,0,0),k={Γ,L}, + compute_q0=false/loto=false;6 位移全收敛、无 NaN、D Hermitian + - **dn≠0 经 q 的折叠实战**:Γ+q→L(dn=0)、L+q→Γ(dn=(1,0,0),383/411 球)—— + 标签匹配 miss=0,整数三元组机制在真 q 下工作 + - **q↔−q 一致性**:D(−L) 特征值与 D(+L) 一致到 0.2-1%(−0.07621/−0.05528/ + −0.03265/−0.02005/−0.00816/+0.06802 vs −0.07607/−0.05518/−0.03249/ + −0.01996/−0.00808/+0.06822)——稀疏 2-k 采样下 q=±L 采样不同跃迁集, + 接近一致即内部自洽;4 个负本征值(虚频)为 2-k 超稀采样的性质,非 q 路径 + bug(±q 忠实重现);物理级验证需密网格/超胞 FD - [ ] B2 输出正式化 - [ ] B3 Kerker 预条件混合 - [ ] B4 数据层收编 From 38cee305b7a1666bb4244d4de8349f53742c55ad Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Wed, 19 Aug 2026 16:32:41 +0800 Subject: [PATCH 24/50] Fix: drop spurious 1/nk in DFPT eps/born sums; wg already carries full BZ weight compute_eps/compute_born divided the band sum by nk, but wg(ik,v) already contains the full k weight wk times the spin factor 2, so the stored-k sum is itself the BZ average. The extra 1/nk was a no-op for Gamma-only runs (nk=1) and scaled down multi-k results by 1/nk. Validation (Si diamond, LDA pz): 4x4x4 sym1 (8 IBZ k) eps_inf diagonal mean = 12.6661; sym0 full-BZ 36 k manual sum = 12.6662 (5-digit cross-mesh agreement; LDA reference ~12.7-13.2, experiment 11.7). Retained the env-gated DFPT_Q0DBG p-matrix dump used for the parity-selection-rule audit. Also documents in PLAN: wfc txt writer G-block (igl2isz FFT-stick order) vs coefficient order (psi-ig) mismatch that invalidates file-based element-level cross-checks, and the O_h parity selection-rule evidence that the in-code p matrices are correct. --- .../module_dfpt/PLAN_dfpt_implementation.md | 24 +++++++++++++++ source/source_pw/module_dfpt/dfpt_q0.cpp | 30 +++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index fbcbdd7931a..8717fea8ecf 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -288,6 +288,30 @@ 工作项 - 案例清单(/tmp/opencode/):k025/k050(已 sym=0 修正)、k222s007/k222s005(+ _fd 配对)、gamma_k222_nosym(守卫触发样本)、kG_mpi2(崩溃样本) + - [x] ε∞ 根因修复:compute_eps/compute_born 的 /nk 额外归一 `(本轮)` + - 根因:`wg(ik,v)` 已含完整 k 权重 wk×自旋 2,χ 求和本身即全 BZ 平均;再除 + nk 对 Γ-only 无害(nk=1 掩盖),多 k 时系统性低估(4×4×4 sym1: ÷8、 + 36-k sym0: ÷36) + - 修复:删除 compute_eps 的 `/ nk`(dfpt_q0.cpp:235)与 compute_born 的 + `/ nk`(:299);DFPT_Q0DBG env 探针(p 矩阵 dump)保留供校准 + - 验证:4×4×4 sym1(8 IBZ k)ε∞ 对角均值 = 12.6661;sym0 全 BZ 36 k = + 12.6662(两网格 5 位一致);LDA pz 参考 ~12.7-13.2、实验 11.7 → 定量达标 + - 撤回两条早期误判:①"8π 应为 16π"——wg 的自旋因子 2 已提供 Ry 能量的 + 2 倍换算,8π 正确;②"p 矩阵 ~2.64× 过大"——基于 wfc txt 文件的 + FD/python 复算全部失效(见下条) + - wfc txt 输出不可用于元素级分析(上游 bug 记录,write_wfc_pw.cpp): + G 块按 igl2isz_k FFT-stick 序打印(|G|² 序列 196/410 处下降,非排序), + 系数按 psi-ig 序打印 → 行配对破坏;identity/stable-sort/lex 重配对均 + 无法恢复宇称。判据 = O_h 宇称选择定则(Γ 小群含反演:v=T2g、c=T1u、 + p=T1u;⟨A1g|T1u|T2g⟩ 严格禁戒):文件行配对给出禁戒元素非零 + (kin(0,x-成员)=0.098、kin(v,band7)~0.5),而代码内部 p 矩阵全部禁戒 + 元素精确为零(1e-16)、(v,band7) 非零 = 非局域交换子 [V_NL,r] 项的 + 合法破缺 → 代码 pos_matrix/build_vkb_dk 链路被选择定则整体背书 + - 遗留:sym1 各向异性(ε 对角 13.78/15.34/8.88、非对角 −5.81)= 缺星 + 旋转(IBZ 张量未绕星转动;迹/均值不受影响,星平均投影子对 sym0/sym1 + 两种 k 列表均合法);Z* 均值 15.6 ≠ 参考 ~4.5,除 /nk 外另有独立 bug + (待查,疑与 dV^κ 位移项相关——dfpt_pert.cpp 含另一会话未提交修改, + 需协调) - [ ] B0 残余:8×8×8 过夜验证(ε∞/Z* 与 D 的密网格收敛);非 Γ q 的物理级验证 (超胞 FD 或色散对照);插桩清理评审 - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index 2699bc9ed70..b12fc031878 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include #include namespace ModuleDFPT { @@ -174,6 +176,26 @@ void DFPT_Q0::pos_matrix(const psi::Psi>& psi, } } } + if (getenv("DFPT_Q0DBG") != nullptr) { + std::cout << "Q0DBG ik=" << ik << " tpiba=" << tpiba + << " npwk=" << npwk << std::endl; + 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); + double p2 = 0.0; + for (int d = 0; d < 3; ++d) { + p2 += std::norm(p_mat[m][n][d]); + } + std::cout << "Q0DBG p m=" << m << " n=" << n + << " de=" << de << " px=" << p_mat[m][n][0] + << " py=" << p_mat[m][n][1] + << " pz=" << p_mat[m][n][2] << std::endl; + } + } + } } } @@ -209,8 +231,11 @@ void DFPT_Q0::compute_eps(const psi::Psi>& psi, } } } + // wg already carries the full k weight (wk) times the spin + // factor 2, so the sum over the stored k list is the complete + // Brillouin-zone average: no extra 1/nk normalization. eps(a, b) = ((a == b) ? 1.0 : 0.0) - + 8.0 * ModuleBase::PI / ucell_->omega * chi / nk; + + 8.0 * ModuleBase::PI / ucell_->omega * chi; } } data.set_dielectric(eps); @@ -274,7 +299,8 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, } } for (int a = 0; a < 3; ++a) { - zstar(a, idir) = -4.0 / nk * acc[a]; + // wg carries the full k weight and spin factor; no extra 1/nk. + zstar(a, idir) = -4.0 * acc[a]; } } // ionic rigid-ion charge on the diagonal (a == b directions) From 3e376d2c5c89913c761aec1aaecb6c65f7f15460 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Wed, 19 Aug 2026 18:04:37 +0800 Subject: [PATCH 25/50] Docs: record continuation plan (P0-1 uncommitted-fix intake, P0-2 Zstar bug, P0-3 B0 closeout, B2-B4, cleanup, A) --- .../module_dfpt/PLAN_dfpt_implementation.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 8717fea8ecf..a105d4b998e 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -133,6 +133,19 @@ - `module_symmetry/little_group.{h,cpp}`:完整不可约表示表 + 投影算子 → 真实 `get_nirr`/`get_mode_basis`(替换占位 =1/空)。 - 测试:金刚石/闪锌矿 Γ/X/L 点 irrep 分解与理论表核对。提交。 +- 收尾:LO-TO 一般方向经 irrep 机制;KVectorUtils 薄封装删除(reciprocal_grid + 重构遗留,随本阶段一并处理)。 + +--- + +## 附注:重构计划(PLAN_reciprocal_grid_refactor.md)状态交叉核对(2026-08-19) + +- Phase 1–4 实质完成:ReciprocalGrid 基类 + K_Vectors/QList 继承 + + LittleGroup 接口占位(nirr≡1)+ DFPT INPUT 驱动接线。 +- 遗留:KVectorUtils 薄封装删除 → 随 A 阶段收尾;LittleGroup 完整 irrep 表 + → 即本计划 A 阶段。 +- 环境注记:当前 build 树未注册 CELL 侧 klist/reciprocal_grid/qlist/ + little_group 测试(需重新 cmake configure 才能跑全量回归)。 --- @@ -314,6 +327,35 @@ 需协调) - [ ] B0 残余:8×8×8 过夜验证(ε∞/Z* 与 D 的密网格收敛);非 Γ q 的物理级验证 (超胞 FD 或色散对照);插桩清理评审 + - [ ] P0-1 未提交修改收编(q≠0 同原子二阶项物理修复 + 调试探针,用户已确认保留路线) + - 背景:工作区 5 文件(dfpt_pert/dfpt_phon/dfpt_pw/dfpt_q0)含两类修改—— + ① 物理修复:同原子 d²V 项两个位移 dressing e^{iq·R} 同乘一原子 → 二阶势 + 携波矢 2q,same-k 期望仅当 2q 折到倒格矢时非零(d2vloc_r 核 w=gcar、 + apply_d2vnl q_eff=fold(2q) + include_middle 门控 |dβ⟩⟨dβ| 中间项、 + accumulate_electron 2q 倒格矢门控);ion_ion 自镜像项去 δ/3 + (D_ii(q)−D_ii(0) 公式经 q 公度超胞 erfc 分裂能量 FD 元素级验证, + α 无关性数值核实);② 调试探针:ZDBG(compute_born 逐 (m,v) 项)、 + BPT(空态 PT 交叉验证 Sternheimer)、NOSC(屏蔽势置零 A/B)、 + D2MID(中间项开关) + - 测试同步(dfpt_phon_serial_test):AccumulateElectronClosedForm 的 + expect_d2 改门控语义(fixture q=(0.13,0,0.07) 非倒格矢 → d2=0); + 新增 2q 倒格矢用例(q=(0.5,0,0) → d2 整数 G 核 + middle 项, + D2MID 语义经 include_middle 直测);IonIonGenericQVsDirectSum + 自镜像参考按新公式重核 + - 顺手:d2vloc_r 已 (void)q_cart 的遗留参数移除(更新调用点,规则 5) + - 构建 + 14 目标回归 + 治理;**拆分两次提交**(物理修复+测试同步; + 调试探针),回写本文件 + - [ ] P0-2 Z* bug 根因与修复(B0 收尾前置,用户已确认优先) + - 现象:Z* 均值 15.6 vs 参考 ~4.5(金刚石);eps 已达标(12.67)→ + pos_matrix/r_mat 链路被选择定则背书,聚焦 compute_born 差异面 + - 首选线索:15.6 ≈ zion(4) + (ε∞−1)·n 的量级 → 疑与 eps 共享因子 + 纠缠(Ω/8π/自旋因子泄漏进 −4 系数链) + - 步骤:① ZDBG 分解 occ-occ vs v→c 块贡献;② BPT 恒等式交叉验证 + dV@q=0 矩阵元;③ 黄金对照 = 小位移偶极 FD(或 Berry 极化)+ + O_h 对称性约束(对角 ~4.5、非对角模式);④ 修复 + + dfpt_q0_serial 增 m 求和结构回归用例 + - [ ] P0-3 B0 收尾:8×8×8 过夜(ε∞/Z*/D 密网格收敛);非 Γ q 物理级验证 + (密 k 色散 vs 超胞 FD);sym1 星旋转各向异性处理或记录在案 - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` - dfpt_qfile + QList::read_from_file 端到端首次运行:q=(0.5,0,0),k={Γ,L}, compute_q0=false/loto=false;6 位移全收敛、无 NaN、D Hermitian @@ -325,6 +367,18 @@ 接近一致即内部自洽;4 个负本征值(虚频)为 2-k 超稀采样的性质,非 q 路径 bug(±q 忠实重现);物理级验证需密网格/超胞 FD - [ ] B2 输出正式化 + - run_post_process design-phase std::cout → 正式输出:多 q 布局、LO-TO + 修正后频率(每方向);loto 方向经数据层传递,消除 run() 中 + (1,1,1)/√3 硬编码;输出格式回归测试 - [ ] B3 Kerker 预条件混合 + - DFPT_Rho 内自实现 |G+q|²/(|G+q|²+a²) 预条件(不引 charge_mixing.h); + mix_type 支持 plain/kerker;验收:λ_A1≈−2.2 模型问题 β=0.7 收敛 + (JPROBE 复用)、金刚石频率与 β 无关、默认 β 回调并文档记录 - [ ] B4 数据层收编 + - 收敛台账(converged_/residuals_/current_iter_ 按 (q,irrep))并入 + DFPT_PW_Data;删除 DFPT_IrrepData 适配层与 get_dpsi_obj static dummy; + 测试迁移;保留 (q,irrep) 接口形状;run() 外层 while 记账语义梳理 + - [ ] 插桩清理评审(B0/B3 后统一):PTCHK/DYNCHK(+2/4/XB)/MDBG/JPROBE/ + VKBCHK/VKBEL/OCCCHK/XB/ZDBG/BPT/NOSC/D2MID/DFPT_MIX_BETA/drho dump + (JPROBE 留 B3 验收后删) - [ ] A irrep 分解(保留接口,工程验证完成后立项) From 2da1a4e836845b9ce3cef59ade455b8f43788be0 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Wed, 19 Aug 2026 18:37:20 +0800 Subject: [PATCH 26/50] Fix: gate same-atom d2V_ext on 2q reciprocal; drop spurious ion_ion delta/3 Physics (intake of the uncommitted 5-file fix, part 1 of 2): - d2vloc_r: both displacement dressings e^{iqR} act on the same atom, so the cell sum collapses to G = 2q (mod ints); the local second-order kernel is nonzero only when 2q is reciprocal and then equals the plain q=0 integer-G kernel. Drop the dead q_cart parameter. - apply_d2vnl: the second-order nonlocal operator carries wavevector 2q; build it on the q_eff = fold(2q)-shifted ball and gate the |dbeta>& q_cart, std::vector>& dv2_r) const { if (pw_rho_ == nullptr) { return; @@ -682,7 +681,12 @@ void DFPT_Pert::d2vloc_r(int atom_idx, int da, int db, ModuleBase::Vector3 gcar; for (int ig = 0; ig < npw; ++ig) { rho_gvec(ig, gcar); - const ModuleBase::Vector3 w = gcar + q_cart; + // both displacement dressings e^{i q.R} multiply on the same atom, so + // the cell sum collapses to G = 2q (mod ints); when the caller's gate + // passes (2q reciprocal) every integer G survives with its own phase + // and the kernel is exactly the plain q=0 one. When the gate fails + // there is no integer solution and the whole term vanishes (skipped). + const ModuleBase::Vector3 w = gcar; const double w2 = w * w; if (w2 < 1.0e-12) { continue; @@ -698,7 +702,8 @@ void DFPT_Pert::d2vloc_r(int atom_idx, int da, int db, } void DFPT_Pert::apply_d2vnl(int atom_idx, int da, int db, - const ModuleBase::Vector3& q_cart, + const ModuleBase::Vector3& q_eff, + bool include_middle, const psi::Psi>& psi, int k_idx, std::vector>>& d2v_psi) const { int it = 0; @@ -740,7 +745,7 @@ void DFPT_Pert::apply_d2vnl(int atom_idx, int da, int db, std::vector>> vkb_in; build_vkb(it, ia, gk_in, vkb_in); DFPT_KQ_Basis kq; - kq.init(pw_wfc_, q_cart, k_idx); + kq.init(pw_wfc_, 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) { @@ -784,14 +789,21 @@ void DFPT_Pert::apply_d2vnl(int atom_idx, int da, int db, dab[mu] += dij * becp_ab[nu]; } } - // chi(G'') = sum_mu vkb_out,mu [ -kq_da kq_db d0 - dab + kq_da db_ + kq_db da_ ]_mu + // chi(G'') = sum_mu vkb_out,mu [ -kq_da kq_db d0 - dab + // + (include_middle ? kq_da db_ + kq_db da_ : 0) ]_mu + // the |d beta>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] + kq_da * db_[mu] + kq_db * da_[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; } diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index add12f04c3d..c6af3f31cb9 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -40,23 +40,31 @@ 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 displacements on - /// the SAME atom; each derivative contributes i w_dir, w = Delta + q, so - /// the reciprocal kernel is -w_da w_db Vloc(|w|) exp(i w.tau)). Returned - /// on the shared real-space grid; its expectation value with |u(r)|^2 - /// enters the electronic dynamical matrix (anharmonic term). + /// 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, - const ModuleBase::Vector3& q_cart, 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 k+q - /// basis (normal-conserving separable case). The four terms come from the - /// phase derivatives of the out (k+q+G'') and in (k+G') projectors of the - /// SAME displaced atom; they reduce to zero for a uniform translation at - /// q=0 (acoustic consistency). + /// 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_cart, + const ModuleBase::Vector3& q_eff, + bool include_middle, const psi::Psi>& psi, int k_idx, std::vector>>& d2v_psi) const; diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index 8c24fb4f023..0953f70d898 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -84,10 +84,16 @@ void DFPT_Phon::ion_ion(const ModuleBase::Vector3& q_frac, // 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: - // sum_{L!=0} h(L)(e^{i2pi q.L} - 1) - // = [erfc piece in the R part] + (4pi/Omega)(sq - s0 - delta_ab/3), - // where the delta/3 is the G=0 limit (w_a w_b / w^2 -> delta_ab/3). + // 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) { @@ -96,10 +102,10 @@ void DFPT_Phon::ion_ion(const ModuleBase::Vector3& q_frac, 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): isotropic limit - for (int d = 0; d < 3; ++d) { - sq[d][d] += 1.0 / 3.0; - } + // 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; @@ -171,7 +177,7 @@ void DFPT_Phon::ion_ion(const ModuleBase::Vector3& q_frac, 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] - (da == db ? 1.0 / 3.0 : 0.0)); + * (sq[da][db] - s0[da][db]); } } } @@ -422,9 +428,25 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, } // ---- same-atom anharmonic term ---- - if (iat == atom_idx && cola >= rowb) { + // both displacement dressings e^{i q.R} multiply on the SAME atom, + // so the second-order potential carries wavevector 2q; the same-k + // expectation value is nonzero only when 2q folds onto a + // reciprocal vector, in which case the operator is + // lattice-periodic and lives on the integer G set (out basis = + // k + q_eff ball with q_eff = fold(2q) = 0). The |d beta> q2_frac = 2.0 * q_frac; + const ModuleBase::Vector3 q2_round(std::round(q2_frac.x), + std::round(q2_frac.y), + std::round(q2_frac.z)); + const bool q2_is_recip = ((q2_frac - q2_round).norm() < 1.0e-8); + const ModuleBase::Vector3 q_eff_cart + = (q2_frac - q2_round) * ucell_->G; + const bool include_middle = true; + if (iat == atom_idx && cola >= rowb && q2_is_recip) { std::vector> dv2_r; - pert_->d2vloc_r(atom_idx, idir, dir, q_cart, 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)); } @@ -436,10 +458,10 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, 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_cart, psi, ik, chi); - // k+q scatter map for this k + 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(), q_cart, ik); + kq.init(pert_->get_pw_wfc(), 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)) { 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 index 5fb597c0689..8820df7eeab 100644 --- 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 @@ -149,13 +149,21 @@ class DFPTPhonSerialTest : public testing::Test ucell_.iat2ia[0] = 0; MakeCoulombAtom(); + SetupBases(k_d_, q_d_); + } + + // (re)initialize the bases and module wiring for a given (k, q) pair; + // SetUp uses the default (k_d_, q_d_) fixture values + void SetupBases(const ModuleBase::Vector3& k_d, + const ModuleBase::Vector3& q_d) + { 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_}; + 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); @@ -163,8 +171,9 @@ class DFPTPhonSerialTest : public testing::Test pw_wfc_.collect_local_pw(); qlist_.nkstot = 1; - qlist_.kvec_d.push_back(q_d_); - q_cart_ = q_d_ * ucell_.G; + qlist_.kvec_d.clear(); + qlist_.kvec_d.push_back(q_d); + q_cart_ = q_d * ucell_.G; data_.init(&qlist_, 1, 2, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); @@ -428,6 +437,9 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) // band 1 unoccupied. k = -q so the k+q basis vectors are plain G''. const int npwk = pw_wfc_.npwk[0]; psi::Psi> psi(1, 2, npwk, npwk, true); + // the buffer is allocated uninitialized; zero it so only the components + // set below are nonzero regardless of heap history from earlier tests + psi.zero_out(); // locate the G=0 plane wave in the k ball: getgpluskcar returns the // cartesian k+G (in 2pi/lat0 units), so look for k+G = k_cart, i.e. G = 0 const ModuleBase::Vector3 k_cart = k_d_ * ucell_.G; @@ -492,24 +504,20 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) * std::complex(std::cos(arg), std::sin(arg)); expect_cross += std::conj(dpsi_inj[igl]) * rhs; } - std::complex expect_d2(0.0, 0.0); - if (adir >= 1) // same-atom anharmonic term, upper triangle only + // the same-atom anharmonic d2 term: 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: the same-k expectation is momentum-forbidden + // and the production gate skips the whole term (zero contribution; + // see the commensurate test below for the gate-on branch) + // Hermitian 2n+1 accumulation: the row element receives + // wg* once per off-diagonal column; the diagonal + // column additionally gets its own conjugate (2 Re) + std::complex expect = wg(0, 0) * expect_cross; + if (adir == 1) { - // |u|^2 = 1 keeps only Delta=0: w = q; the production path - // accumulates the d2V expectation with the occupation weight - const double w2 = q_cart_ * q_cart_; - const double arg = -ModuleBase::TWO_PI * (q_cart_ * tau_); - expect_d2 = -(ucell_.tpiba * q_cart_[1]) * (ucell_.tpiba * q_cart_[adir]) - * VlocCoulomb(w2 * ucell_.tpiba2) - * std::complex(std::cos(arg), std::sin(arg)) - * wg(0, 0); + expect = 2.0 * expect.real(); } - // accumulate divides term2 by sqrt(m_a m_b) and d2V by m (m = 12 here) - const std::complex expect - = (2.0 * wg(0, 0) * expect_cross + expect_d2) / ucell_.atoms[0].mass; - // note: accumulate uses (da=adir for the column, db=1 for the row); - // d2vloc_r multiplies w_da w_db symmetrically, so the closed form - // above (dir1 x adir) matches either ordering + 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))) @@ -527,6 +535,219 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) } } +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 + 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; + int ig_zero = -1; + 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) + { + ig_zero = ig; + break; + } + } + ASSERT_GE(ig_zero, 0); + psi(0, 0, ig_zero) = std::complex(1.0, 0.0); + ModuleBase::matrix wg(1, 2, true); + wg(0, 0) = 2.0; + wg(0, 1) = 0.0; + + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, 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_); + + for (int adir = 0; adir < 3; ++adir) + { + std::complex expect_cross(0.0, 0.0); + for (int igl = 0; igl < kq.get_npwk(); ++igl) + { + const ModuleBase::Vector3 g = kq.get_gpluskq(igl); + const ModuleBase::Vector3 w = g + q_cart_; + const double w2 = w * w; + if (w2 < 1.0e-12) + { + continue; + } + const double arg = -ModuleBase::TWO_PI * (w * tau_); + const std::complex rhs = std::complex(0.0, -1.0) + * (ucell_.tpiba * w[adir]) + * VlocCoulomb(w2 * ucell_.tpiba2) + * std::complex(std::cos(arg), + std::sin(arg)); + expect_cross += std::conj(dpsi_inj[igl]) * rhs; + } + std::complex expect = wg(0, 0) * expect_cross; + 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); + SetupBases(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 int ncomp = 3; + const ModuleBase::Vector3 gfrac[ncomp] + = {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::complex ccoef[ncomp] = {std::complex(1.0, 0.0), + std::complex(0.6, -0.3), + std::complex(-0.4, 0.25)}; + ModuleBase::Vector3 gcart[ncomp]; + int ig_of[ncomp] = {-1, -1, -1}; + for (int 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 (int 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 (int ic = 0; ic < ncomp; ++ic) + { + ASSERT_GE(ig_of[ic], 0); + psi(0, 0, ig_of[ic]) = ccoef[ic]; + } + ModuleBase::matrix wg(1, 2, true); + wg(0, 0) = 2.0; + wg(0, 1) = 0.0; + + // injected dpsi on the k+q = 0 ball (arbitrary coefficients) + ModuleDFPT::DFPT_KQ_Basis kq; + kq.init(&pw_wfc_, q_cart_, 0); + const int npwk_kq = kq.get_npwk(); + std::vector> dpsi_inj(npwk_kq, std::complex(0.0, 0.0)); + dpsi_inj[0] = std::complex(0.3, 0.1); + if (npwk_kq > 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) + { + // cross term: RHS(G'') = sum_i c_i (-i) tpiba w_{i,a} Vloc(|w_i|^2) + // e^{-i2pi w_i.tau} with w_i = G'' - G'_i + q + std::complex expect_cross(0.0, 0.0); + for (int igl = 0; igl < npwk_kq; ++igl) + { + const ModuleBase::Vector3 gpp = kq.get_gpluskq(igl); // = G'' + for (int ic = 0; ic < ncomp; ++ic) + { + const ModuleBase::Vector3 w = gpp - gcart[ic] + q_cart_; + const double w2 = w * w; + if (w2 < 1.0e-12) + { + continue; + } + const double arg = -ModuleBase::TWO_PI * (w * tau_); + const std::complex rhs = std::complex(0.0, -1.0) + * (ucell_.tpiba * w[adir]) + * VlocCoulomb(w2 * ucell_.tpiba2) + * std::complex(std::cos(arg), + std::sin(arg)); + expect_cross += ccoef[ic] * std::conj(dpsi_inj[igl]) * rhs; + } + } + std::complex expect = wg(0, 0) * expect_cross; + 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 (int i = 0; i < ncomp; ++i) + { + for (int 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 // --------------------------------------------------------------------------- From 4801371679c3868865a338c8c730d71a71024e33 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Wed, 19 Aug 2026 18:37:59 +0800 Subject: [PATCH 27/50] Debug: DFPT design-phase probes (ZDBG/BPT/NOSC/D2MID/DYNCHK/XB) Part 2 of 2 of the uncommitted-fix intake: env-gated diagnostic probes for the P0-2 Z* investigation and B-phase A/B debugging, all no-ops when their env vars are unset (tracked for cleanup in PLAN_dfpt_implementation.md probe ledger): - DFPT_ZDBG (dfpt_q0 compute_born): per-occ-state decomposition of the Born-charge summand (wg, energy denominator, dV matrix element, position matrix element) to split occ-occ vs valence contributions. - DFPT_BPT (dfpt_pw): perturbation-theory cross-check of the Sternheimer solve, vs sum_m ||^2/(e_m-e_n) over the empty manifold at k+q (empty_kq_ cache added). - DFPT_NOSC (dfpt_pw): zero the screened potential to isolate the bare Sternheimer response. - DFPT_D2MID / DYNCHK d2gate (dfpt_phon): disable the |dbeta>build_dv(q_idx, iat, idir, data); const bool dbg2 = (getenv("DFPT_DEBUG") != nullptr); - const bool xbk = (getenv("DFPT_XB") != nullptr && rowb == 0 - && (cola == 0 || cola == 3 || cola == 1)); + const bool xbk = (getenv("DFPT_XB") != nullptr + && (rowb == 0 || rowb == 6) + && (cola == 0 || cola == 3 || cola == 1 + || cola == 6)); std::complex cross(0.0, 0.0); std::vector> cross_k; for (int ik = 0; ik < nk; ++ik) { @@ -435,7 +437,8 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, // lattice-periodic and lives on the integer G set (out basis = // k + q_eff ball with q_eff = fold(2q) = 0). The |d beta> q2_frac = 2.0 * q_frac; const ModuleBase::Vector3 q2_round(std::round(q2_frac.x), std::round(q2_frac.y), @@ -443,7 +446,21 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, const bool q2_is_recip = ((q2_frac - q2_round).norm() < 1.0e-8); const ModuleBase::Vector3 q_eff_cart = (q2_frac - q2_round) * ucell_->G; - const bool include_middle = true; + const bool q_is_recip + = ((q_frac + - ModuleBase::Vector3(std::round(q_frac.x), + std::round(q_frac.y), + std::round(q_frac.z))) + .norm() + < 1.0e-8); + const char* d2mid_env = getenv("DFPT_D2MID"); + const bool include_middle = !(d2mid_env != nullptr && d2mid_env[0] == '0'); + if (dbg2 && iat == atom_idx && cola == rowb) { + std::cout << "DYNCHK d2gate rowb=" << rowb + << " q2recip=" << (q2_is_recip ? 1 : 0) + << " qrecip=" << (q_is_recip ? 1 : 0) + << " mid=" << (include_middle ? 1 : 0) << std::endl; + } if (iat == atom_idx && cola >= rowb && q2_is_recip) { std::vector> dv2_r; pert_->d2vloc_r(atom_idx, idir, dir, dv2_r); diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index ff7b3a4230b..97605fb4d3f 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -69,6 +69,9 @@ class DFPT_PW::Impl { ///< 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_; + ///< BPT debug: empty states at k+q on the k+q G list + their eigenvalues + std::vector>>> empty_kq_; + std::vector> empty_kq_eig_; ///< remembers the (q_idx, ik) the shifted operator was last cached at int last_q_ = -1; int last_ik_ = -1; @@ -199,6 +202,11 @@ bool DFPT_PW::get_u_active() const { void DFPT_PW::Impl::build_occ_kq(int q_idx) { const int nk = pw_wfc_->nks; occ_kq_.assign(nk, std::vector>>()); + // BPT debug companion: empty states at k+q on the same kq ball, for the + // independent perturbation-theory cross-check of the Sternheimer solve + // (DFPT_BPT); eig pairs are (eig_(ikq, m), eig_(ik, n)) + empty_kq_.assign(nk, std::vector>>()); + empty_kq_eig_.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; @@ -267,10 +275,12 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { } const int nbands = gs_psi_.get_nbands(); + const bool want_empty = (getenv("DFPT_BPT") != nullptr); int dbg_miss = 0; int dbg_tot = 0; for (int m = 0; m < nbands; ++m) { - if (!dfpt_band_occupied(wg_, ikq, m)) { + const bool occ_m = dfpt_band_occupied(wg_, ikq, m); + if (!occ_m && !want_empty) { continue; // empty at k+q: outside the P_c projector } std::vector> state(npw_kq, std::complex(0.0, 0.0)); @@ -288,7 +298,12 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { } ++dbg_tot; } - occ_kq_[ik].push_back(std::move(state)); + if (occ_m) { + occ_kq_[ik].push_back(std::move(state)); + } else { + empty_kq_[ik].push_back(std::move(state)); + empty_kq_eig_[ik].push_back(eig_(ikq, m)); + } } if (getenv("DFPT_DEBUG") != nullptr) { std::cout << "OCCCHK ik=" << ik << " ikq=" << ikq @@ -517,6 +532,11 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } } } + // DFPT_NOSC: zero the screened part to isolate the bare Sternheimer + // (design-phase A/B knob for term2 debugging) + if (getenv("DFPT_NOSC") != nullptr) { + std::fill(v_sc_r.begin(), v_sc_r.end(), std::complex(0.0, 0.0)); + } v_sc_r_last = v_sc_r; // ---- 2. Sternheimer solve of every occupied (k, band) @@ -608,6 +628,48 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { << std::endl; } data_.set_dpsi(q_idx, ik, ib, dpsi_out); + // BPT: independent PT cross-check of the solve for this band. + // Identity (exact on a complete empty manifold): + // == sum_m ||^2 / (e_m(k+q) - e_n(k)) + // rhs here is the TOTAL (ext+sc) right-hand side with the + // Sternheimer sign (rhs = -(ext+sc)); both sides use the same + // object so the sign cancels on the diagonal-identity check. + if (getenv("DFPT_BPT") != nullptr + && static_cast(empty_kq_[ik].size()) > 0 + && dpsi_out.size() == rhs.size()) { + std::complex codedot(0.0, 0.0); + double pt = 0.0; + double wsum = 0.0; + for (size_t i = 0; i < dpsi_out.size(); ++i) { + codedot += std::conj(dpsi_out[i]) * rhs[i]; + } + for (size_t im = 0; im < empty_kq_[ik].size(); ++im) { + const std::vector>& psim + = empty_kq_[ik][im]; + if (psim.size() != rhs.size()) { + continue; + } + std::complex mdot(0.0, 0.0); + for (size_t i = 0; i < rhs.size(); ++i) { + mdot += std::conj(psim[i]) * rhs[i]; + } + const double denom = empty_kq_eig_[ik][im] - eig_(ik, ib); + if (std::abs(denom) > 1.0e-10) { + pt += std::norm(mdot) / denom; + } + wsum += std::norm(mdot); + } + double nrm2 = 0.0; + for (size_t i = 0; i < rhs.size(); ++i) { + nrm2 += std::norm(rhs[i]); + } + std::cout << "BPTCHK q=" << q_idx << " iat=" << iat + << " idir=" << idir << " ik=" << ik << " ib=" << ib + << " code=(" << codedot.real() << "," << codedot.imag() << ")" + << " pt=" << pt + << " ||^2sum=" << wsum + << " |rhs|^2=" << nrm2 << std::endl; + } } } diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index b12fc031878..ad1aee410a4 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -247,6 +247,7 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, if (ucell_ == nullptr || pert_ == nullptr) { return; } + const bool zdbg = getenv("DFPT_ZDBG") != nullptr; std::vector>>>> r_mat; pos_matrix(psi, eig, r_mat); const int nk = psi.get_nk(); @@ -289,6 +290,15 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, for (size_t ig = 0; ig < rhs.size(); ++ig) { dv_mv += std::conj(psi(ik, m, ig)) * rhs[ig]; } + if (zdbg) { + const ModuleBase::Vector3>& ra + = r_mat[ik][m][v]; + std::cout << "ZDBG iat=" << iat << " idir=" << idir + << " ik=" << ik << " v=" << v << " m=" << m + << " wg=" << wg(ik, v) << " de=" << de + << " dv=" << dv_mv << " r=" << ra + << std::endl; + } // = conj(dv_mv), multiplied from the // right by (Gonze-Lee ordering) for (int a = 0; a < 3; ++a) { From 4db12d7a5bf5bfd67e03ae22cf61517418aa5a85 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Wed, 19 Aug 2026 18:39:12 +0800 Subject: [PATCH 28/50] Docs: P0-1 done (2q-reciprocal d2 gate intake, order-dependence fix, 28/28 serial) --- .../module_dfpt/PLAN_dfpt_implementation.md | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index a105d4b998e..3f19b67a365 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -327,7 +327,8 @@ 需协调) - [ ] B0 残余:8×8×8 过夜验证(ε∞/Z* 与 D 的密网格收敛);非 Γ q 的物理级验证 (超胞 FD 或色散对照);插桩清理评审 - - [ ] P0-1 未提交修改收编(q≠0 同原子二阶项物理修复 + 调试探针,用户已确认保留路线) + - [x] P0-1 未提交修改收编(q≠0 同原子二阶项物理修复 + 调试探针,用户已确认保留路线) + `(完成,commit 2da1a4e83 物理+测试 / 480137167 探针)` - 背景:工作区 5 文件(dfpt_pert/dfpt_phon/dfpt_pw/dfpt_q0)含两类修改—— ① 物理修复:同原子 d²V 项两个位移 dressing e^{iq·R} 同乘一原子 → 二阶势 携波矢 2q,same-k 期望仅当 2q 折到倒格矢时非零(d2vloc_r 核 w=gcar、 @@ -337,14 +338,25 @@ α 无关性数值核实);② 调试探针:ZDBG(compute_born 逐 (m,v) 项)、 BPT(空态 PT 交叉验证 Sternheimer)、NOSC(屏蔽势置零 A/B)、 D2MID(中间项开关) - - 测试同步(dfpt_phon_serial_test):AccumulateElectronClosedForm 的 - expect_d2 改门控语义(fixture q=(0.13,0,0.07) 非倒格矢 → d2=0); - 新增 2q 倒格矢用例(q=(0.5,0,0) → d2 整数 G 核 + middle 项, - D2MID 语义经 include_middle 直测);IonIonGenericQVsDirectSum - 自镜像参考按新公式重核 + - 测试同步(dfpt_phon_serial_test):AccumulateElectronAnalyticContraction 的 + expect 同步 Hermitian 2n+1 累积约定(dc82fac9b)+ 门控语义 + (fixture q=(0.13,0,0.07) 非倒格矢 → d2=0);新增 + AccumulateElectronD2GateOffGenericQ(行 0 纯 cross 锐探针)与 + AccumulateElectronD2CommensurateQ(k=(−½,0,0), q=(½,0,0),三分量 ψ + 钉死 cross 与 d2 核 K_{ab}(G)=−tpiba²·G_a·G_1·Vloc·e^{−i2πG·τ}, + 含 K(G_i−G_j) 负谐波约定:实空间 |u|² 收缩挑出核的负谐波,闭式须跑 + K(G_i−G_j) 配 c_i* c_j) + - **顺序依赖缺陷根除**:psi::Psi 构造只 malloc 不清零("no_record"), + 测试未显式置零的分量读堆垃圾 → 单跑恰逢零页通过、全量套件被前面测试 + 脏堆污染而挂;三处构造后补 psi.zero_out();pert/q0 套件复核已有 + zero_out/全量填充,无同类问题;--gtest_shuffle ×3 稳定 - 顺手:d2vloc_r 已 (void)q_cart 的遗留参数移除(更新调用点,规则 5) - - 构建 + 14 目标回归 + 治理;**拆分两次提交**(物理修复+测试同步; - 调试探针),回写本文件 + - 验证:cmake 重配置注册 CELL 测试后 12/12 通过 + (klist/reciprocal_grid/qlist/little_group + DFPT 8 套件);DFPT 串行 + 28/28(phon 9 + pert 8 + q0 5 + rho 6);治理检查 HEAD~2..HEAD 零新增 + ERROR、仅 1 条 docs-sync 警告(无 INPUT 行为变化,无需文档更新) + - **拆分两次提交**(物理修复+测试同步 2da1a4e83;调试探针 480137167), + 本文件回写即本次提交 - [ ] P0-2 Z* bug 根因与修复(B0 收尾前置,用户已确认优先) - 现象:Z* 均值 15.6 vs 参考 ~4.5(金刚石);eps 已达标(12.67)→ pos_matrix/r_mat 链路被选择定则背书,聚焦 compute_born 差异面 From 0d510b8d5f91ac79209af3c03754f0412779af64 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Wed, 19 Aug 2026 20:44:38 +0800 Subject: [PATCH 29/50] DFPT q0: star-rotate the symmetry-reduced eps/Z* tensor sums Symmetry-reduced k sums of the q=0 susceptibility tensors must be star-averaged: the partial at a rotated star member Rk is R chi(k) R^T (cartesian column form), with atom-resolved Born partials credited to the image atom under the paired direct-space operation. The row-form operator G^-1*kgmatrix*G from the kvec_d row convention had been fed to rotate_tensor untransposed, which breaks the star sum (right- vs left-coset representatives), so store the transpose. Diamond Si 4x4x4 verification (sym=1): eps_inf = 12.6661*I and Z* = 15.5799*I per atom, both bit-consistent with the symmetry-off full-mesh reference (off-diagonals ~1e-14; previously 13.78/15.34/8.88 anisotropic). The remaining Z* offset vs the diamond target 0 is the known missing-screening formula defect, tracked as the next P0-2 item. Add StarRotationCyclicGroup to dfpt_q0_serial (C3 orbit cell: star size, anisotropic trace-6 tensor averaging to 2*I, cyclic atom maps, identity fallback) and the DFPT_STARDBG probe; build_stars/rotate_tensor/stars_ move to public for the test. --- .../module_dfpt/PLAN_dfpt_implementation.md | 31 ++ source/source_pw/module_dfpt/dfpt_q0.cpp | 275 ++++++++++++++++-- source/source_pw/module_dfpt/dfpt_q0.h | 46 ++- .../test_serial/dfpt_q0_serial_test.cpp | 136 +++++++++ 4 files changed, 449 insertions(+), 39 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 3f19b67a365..cdfda65a2dc 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -366,6 +366,37 @@ dV@q=0 矩阵元;③ 黄金对照 = 小位移偶极 FD(或 Berry 极化)+ O_h 对称性约束(对角 ~4.5、非对角模式);④ 修复 + dfpt_q0_serial 增 m 求和结构回归用例 + - **根因判定(完成,两个独立缺陷)**: + 1. **星旋转缺失(已修复,本轮提交)**:sym=1 归约 k 表上直接 + 求和无星平均 → 强各向异性(ε∞ 13.78/15.34/8.88、Z* + 16.72/17.76/12.27)。修复:dfpt_q0 新增 build_stars/ + rotate_tensor/星积分(成员=折叠去重的 kgmatrix 星点, + cart=列形式旋转,atom_map=gmatrix+gtrans 的像原子, + nrotk<=0 或映射失败回退恒等);Z* 部分张量按像原子记账 + (反演伴星换原子,χ₀(−k)=χ₁(k) 已数值验证 2e-5)。 + **关键坑**:`Vector3*Matrix3` 行乘积 G⁻¹KG 是行约定算符, + 张量旋转要列形式 → rotate_tensor 直接用会得 P^T χ P; + {P_m⁻¹k} 是右陪集代表系非左 → 星求和真被破坏(ε∞ 也错), + 必须存转置(dfpt_q0.cpp 构造处已注明) + 2. **公式级缺陷(待修,下一步)**:独立粒子求和 + /(ε_m−ε_v) 缺屏蔽响应(Sternheimer + 2n+1 形式)。金刚石基准(nosym 全网格、星记账正确值): + ε∞=12.6661·δ ✓ 公式正确勿动;Z*=15.5799·δ(ASR 违背: + 电子部分 +11.58/atom vs 应 −4;O_h+反演下金刚石 Z*≡0) + —— plan 早期"参考 ~4.5"来自陈旧日志,金刚石正确目标为 0 + - 修复后验证(zstar_sym3,sym=1 4×4×4):ε∞=12.6661·δ、 + Z*=15.5799·δ,两者均与 nosym 全网格参照逐位一致(非对角 + ~1e-14)→ 星旋转机械精确 + - 新增回归用例 StarRotationCyclicGroup(dfpt_q0_serial):sc 胞 + C3 轨道 3 原子 + k=(¼,0,0),钉死星大小 3、各向异性 + trace-6 张量星平均=2δ、循环 atom_map 覆盖 {0,+1,+2} 位移、 + nrotk=0 恒等回退;build_stars/rotate_tensor/stars_ 移至 + public 供测试 + - 探针:新增 DFPT_STARDBG(build_stars 转储 kgmatrix/gmatrix/ + gtrans/成员 cart/amap),与 ZDBG 等同登记待清理 + - 离线验证工具(/tmp/opencode,不入库):star_check.py/ + star_debug.py/star_compare.py(几何星 vs 代码星逐成员比对, + 定位转置缺陷);truth_check.py(nosym 重建基准) - [ ] P0-3 B0 收尾:8×8×8 过夜(ε∞/Z*/D 密网格收敛);非 Γ q 物理级验证 (密 k 色散 vs 超胞 FD);sym1 星旋转各向异性处理或记录在案 - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index ad1aee410a4..6aeeb52039c 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -30,6 +30,171 @@ void DFPT_Q0::init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, pw_rho_ = pw_rho; pw_wfc_ = pw_wfc; pert_ = pert; + stars_.clear(); +} + +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; + } +} + +// 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::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 bool stardbg = getenv("DFPT_STARDBG") != nullptr; + const int nat = ucell_->nat; + if (stardbg) { + std::cout << "STARDBG nrotk=" << symm.nrotk << " nat=" << nat << std::endl; + for (int j = 0; j < symm.nrotk; ++j) { + std::cout << "STARDBG j=" << j << " kg=[" << symm.kgmatrix[j].e11 << "," + << symm.kgmatrix[j].e12 << "," << symm.kgmatrix[j].e13 << ";" + << symm.kgmatrix[j].e21 << "," << symm.kgmatrix[j].e22 << "," + << symm.kgmatrix[j].e23 << ";" << symm.kgmatrix[j].e31 << "," + << symm.kgmatrix[j].e32 << "," << symm.kgmatrix[j].e33 << "] g=[" + << symm.gmatrix[j].e11 << "," << symm.gmatrix[j].e12 << "," + << symm.gmatrix[j].e13 << ";" << symm.gmatrix[j].e21 << "," + << symm.gmatrix[j].e22 << "," << symm.gmatrix[j].e23 << ";" + << symm.gmatrix[j].e31 << "," << symm.gmatrix[j].e32 << "," + << symm.gmatrix[j].e33 << "] gt=(" << symm.gtrans[j].x << "," + << symm.gtrans[j].y << "," << symm.gtrans[j].z << ")" << std::endl; + } + } + 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); + if (stardbg) { + std::cout << "STARDBG ik=" << ik << " j=" << j << " kp=(" << kp.x + << "," << kp.y << "," << kp.z << ") cart=[" << mem.cart.e11 + << "," << mem.cart.e12 << "," << mem.cart.e13 << ";" + << mem.cart.e21 << "," << mem.cart.e22 << "," << mem.cart.e23 + << ";" << mem.cart.e31 << "," << mem.cart.e32 << "," + << mem.cart.e33 << "]" << std::endl; + } + // 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; + } + if (stardbg) { + std::cout << "STARDBG ik=" << ik << " j=" << j << " amap="; + for (int iat2 = 0; iat2 < nat; ++iat2) { + std::cout << mem.atom_map[iat2] << (iat2 + 1 < nat ? "," : ""); + } + std::cout << std::endl; + } + 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, @@ -209,33 +374,54 @@ void DFPT_Q0::compute_eps(const psi::Psi>& psi, pos_matrix(psi, eig, r_mat); const int nk = psi.get_nk(); const int nbands = psi.get_nbands(); - ModuleBase::matrix eps(3, 3, true); - for (int a = 0; a < 3; ++a) { - for (int b = 0; b < 3; ++b) { - double chi = 0.0; - 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 c = 0; c < nbands; ++c) { - if (dfpt_band_occupied(wg, ik, c)) { - continue; // occupied - } - const double de = eig(ik, c) - eig(ik, v); - if (std::abs(de) < 1.0e-8) { - continue; - } - chi += wg(ik, v) + build_stars(nk); + // wg-weighted partial susceptibility chi_k[ik](a, b) at every stored k + 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 c = 0; c < nbands; ++c) { + if (dfpt_band_occupied(wg, ik, c)) { + continue; // occupied + } + const double de = eig(ik, c) - eig(ik, v); + if (std::abs(de) < 1.0e-8) { + continue; + } + for (int a = 0; a < 3; ++a) { + for (int b = 0; b < 3; ++b) { + chi_k[ik](a, b) + += wg(ik, v) * (r_mat[ik][v][c][a] * r_mat[ik][c][v][b]).real() / de; } } } - // wg already carries the full k weight (wk) times the spin - // factor 2, so the sum over the stored k list is the complete - // Brillouin-zone average: no extra 1/nk normalization. - eps(a, b) = ((a == b) ? 1.0 : 0.0) - + 8.0 * ModuleBase::PI / ucell_->omega * chi; + } + } + 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) { + eps(a, b) *= 8.0 * ModuleBase::PI / ucell_->omega; + if (a == b) { + eps(a, b) += 1.0; + } } } data.set_dielectric(eps); @@ -263,13 +449,18 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, } } + 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) { - ModuleBase::matrix zstar(3, 3, true); + // 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) { // dV matrix elements at q = 0 through the C1 path; apply_dv // delivers dV|u_v> on the k+q = k basis for every k. pert_->build_dv(0, iat, idir, data); - std::vector acc(3, 0.0); for (int ik = 0; ik < nk; ++ik) { pert_->apply_dv(0, ik, psi, data); for (int v = 0; v < nbands; ++v) { @@ -302,15 +493,39 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, // = conj(dv_mv), multiplied from the // right by (Gonze-Lee ordering) for (int a = 0; a < 3; ++a) { - acc[a] += wg(ik, v) - * (std::conj(dv_mv) * r_mat[ik][m][v][a]).real() / de; + chi_k[ik](a, idir) + += wg(ik, v) + * (std::conj(dv_mv) * r_mat[ik][m][v][a]).real() / de; } } } } - for (int a = 0; a < 3; ++a) { - // wg carries the full k weight and spin factor; no extra 1/nk. - zstar(a, idir) = -4.0 * acc[a]; + } + // 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) = -4.0 * zacc[iat](a, d); } } // ionic rigid-ion charge on the diagonal (a == b directions) diff --git a/source/source_pw/module_dfpt/dfpt_q0.h b/source/source_pw/module_dfpt/dfpt_q0.h index ca25633e22f..2c96fd56c53 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.h +++ b/source/source_pw/module_dfpt/dfpt_q0.h @@ -37,11 +37,16 @@ class DFPT_Pert; * 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) / Nk + * * Re[] / (eps_c - eps_v) * Born charges from dP/dtau (King-Smith/Resta Berry phases; the m sum runs * over ALL bands, occupied and empty, m != v): - * Z*_k,ab = Z_k delta_ab - (4/Nk) sum_{k,v occ,m!=v} wg + * Z*_k,ab = Z_k delta_ab - 4 sum_{k,v occ,m!=v} wg * * Re[] / (eps_m - eps_v) + * 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 bare displacement potential dV/dtau comes from DFPT_Pert (C1) at * q = 0; the absolute calibration of both expressions is pinned by the * diamond end-to-end test in C7 (structure/symmetry by the C6 tests). @@ -50,27 +55,50 @@ class DFPT_Q0 { public: DFPT_Q0(); ~DFPT_Q0(); - - void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, + + void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, ModulePW::PW_Basis_K* pw_wfc, DFPT_Pert* pert); - + void compute_eps(const psi::Psi>& psi, const ModuleBase::matrix& wg, const ModuleBase::matrix& eig, 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). 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; 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 index ebcffe7258e..3aaf734004d 100644 --- 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 @@ -716,3 +716,139 @@ TEST_F(DFPTQ0SerialTest, ComputeBornAnalyticCoulomb) 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); + } + } +} From 8124a7f13ee48690cd296d4261a1136aaeca2a90 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Thu, 20 Aug 2026 13:55:44 +0800 Subject: [PATCH 30/50] DFPT q0: Sternheimer screened Z* (v4), QE-anchored eps 16pi fix, zstar_eu cross-check probe - solve_pos_resp + compute_born v4: Y^a = (H-eps_v)^-1 P_c [H,x_a]|psi> (velocity rhs, build_vkb_dk nonlocal part), Z* = zion delta - 2 sum wg Re (QE add_zstar_ue form); pos_resp/ dpsi_efield stashes in DFPT_PW_Data - eps factor 2 fix: 16 pi / Omega per QE dielec.f90 (8 pi was half); ComputeEpsTwoLevelAnalytic expectation synced, serial 6/6 - DFPT_ALEG probe: E-field SCF fixed point (solve_e form) + zstar_eu A-leg vs zstar_ue B-leg cross-check + SCF eps + DFPT_PTCROSS bare cross spectral diagnostic - validated vs locally built QE 7.2 (same UPF/cell/ecut/mesh): GS energy identical, Gamma-TO 517.5/517.6 vs 517.63 (0.03%), Z* -1.19928 vs -1.19765 (0.14%), eps_scf 23.6825 vs 23.6685 (0.06%); 4x4x4 anomaly (Z*=-1.2, eps~23.7 vs lit 13) shown to be shared k-mesh convergence by QE discriminators (ONCV@4x4x4 same, pz-vbc@8x8x8 -> 14.04/-0.09) - PLAN P0-2 closed with validation matrix and re-scoped acceptance --- source/source_esolver/esolver_dfpt_pw.cpp | 35 + .../module_dfpt/PLAN_dfpt_implementation.md | 36 + source/source_pw/module_dfpt/dfpt_pw.cpp | 698 +++++++++++++++++- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 38 + source/source_pw/module_dfpt/dfpt_pw_data.h | 29 +- source/source_pw/module_dfpt/dfpt_q0.cpp | 86 +-- source/source_pw/module_dfpt/dfpt_q0.h | 30 +- .../test_serial/dfpt_q0_serial_test.cpp | 98 +-- 8 files changed, 935 insertions(+), 115 deletions(-) diff --git a/source/source_esolver/esolver_dfpt_pw.cpp b/source/source_esolver/esolver_dfpt_pw.cpp index 72652b82c82..0c5690b671c 100644 --- a/source/source_esolver/esolver_dfpt_pw.cpp +++ b/source/source_esolver/esolver_dfpt_pw.cpp @@ -18,8 +18,10 @@ #include "source_pw/module_dfpt/dfpt_pw.h" #include "source_pw/module_dfpt/dfpt_rho.h" +#include #include #include +#include #include namespace { @@ -92,6 +94,39 @@ class XC_First_Order_FDM : public ModuleDFPT::XC_First_Order { 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) { diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index cdfda65a2dc..1f51cbc6ff7 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -397,6 +397,42 @@ - 离线验证工具(/tmp/opencode,不入库):star_check.py/ star_debug.py/star_compare.py(几何星 vs 代码星逐成员比对, 定位转置缺陷);truth_check.py(nosym 重建基准) + - **公式级缺陷 v4(完成):Sternheimer 屏蔽 Z\* 全链落地** + - 实现:solve_pos_resp 解 Y^a=(H−ε_v)⁻¹P_c[H,x_a]|ψ⟩(rhs + −(i/tpiba)·dH/dk|ψ⟩,vel=2tpiba²·gk 即 tpiba·dH/dk,恰消 + 归一,QE commutator_Hx_psi 同构;非局域 dk 项 build_vkb_dk + 经 DFPT_DKCHK 中心差分逐 μ 验证);compute_born 收缩 + Z*=zion·δ−2Σwg Re⟨dpsi^κ,scf|Y^a⟩(QE add_zstar_ue 锚定, + m-empty 完备性经 occ-occ pairwise 反对称对消+Sternheimer + 平行输运规范证明);dpsi_disp/pos_resp/set_vsc_r 三套 stash + - 验证矩阵(每项均独立排除一类缺陷):DFPT_XCDBG(f_xc 核 + 中心差分自检,PZ 符号/量级正确);DFPT_XCS 扫描(光滑线性 + 定点);DFPT_ALEG zstar_eu 交叉腿 A==B 逐位一致(收缩/双 + stash 豁免;E 场 SCF 定点与 κ 场自洽);DFPT_PTCROSS 裸交 + 叉谱分解(M⁻¹ 对角+交叉 5 位一致、厄米精确、nb8→16 稳定) + - Berry FD 外锚(本仓 GS berry_phase,δ=0.05 bohr 三结构): + P(u) 斜率 <1e-5 → Z*≡0 目标由独立机制确认 + - **外部锚点战役(QE 7.2 本地串行构建,同 UPF/胞/ecut/网格)**: + GS 能量逐位一致(−215.426 eV);Γ 声子 TO 517.63 vs 我们 + 517.5/517.606/517.722(0.03%);Z*=−1.19765 vs 我们 −1.19928 + (0.1%);ε∞=23.668=2×我们的 11.341+1(0.05%) + - **唯一真 bug:ε∞ 缺因子 2(已修)**:QE dielec.f90 实锚 + ε=δ−4·(4π/Ω)·wk·Re⟨Y^i|dpsi^E,j⟩=16π/Ω 形;compute_eps 与 + ALEG 探针同步 8π→16π,ComputeEpsTwoLevelAnalytic 期望值同步, + 串行 6/6 + - **s=1.29982 之谜消解**:非代码缺陷。QE 判别实验(换赝势 + ONCV@4×4×4 仍 23.49/−1.169;加密 pz-vbc@8×8×8 → 14.04/ + −0.092→0)证明 Γ 中心 4×4×4 网格收敛误差为两代码共享的 + 物理量级;κκ 干净(s⁰)、κE ×s、EE ×s² 的通道指纹即 + Y 族对网格收敛的敏感性分层 + - **验收口径修订**:4×4×4 下验收值=QE 同网格参照 + (Z*≈−1.20·δ、ε∞_scf≈23.68、TO 517.5±0.2、声学 |·|<20); + Z*→0 对称性目标移交 P0-3 8×8×8 过夜(QE@8×8×8 已预示 + −0.09→0) + - 探针登记(P0-3 清理评审):DFPT_ALEG(solve_efield_resp+ + aleg_crosscheck+PTCROSS,dfpt_pw.cpp)、DFPT_XCS/XCDBG/ + DKCHK/NOXC/NOSC/YCHK/BPT/MDBG/JPROBE;dpsi_efield stash + (dfpt_pw_data)随 ALEG 保留 - [ ] P0-3 B0 收尾:8×8×8 过夜(ε∞/Z*/D 密网格收敛);非 Γ q 物理级验证 (密 k 色散 vs 超胞 FD);sym1 星旋转各向异性处理或记录在案 - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 97605fb4d3f..b50c0aa44cf 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -92,6 +92,22 @@ class DFPT_PW::Impl { /// 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); design-phase + /// probe behind DFPT_ALEG for the zstar_eu cross-check + void solve_efield_resp(int q_idx); + + /// zstar_eu cross-check of the screened Born charges (dpsi^E,scf + /// contracted with the bare dV^kappa|psi> legs) against the zstar_ue + /// form compute_born produced (DFPT_ALEG probe) + void aleg_crosscheck(int q_idx); }; DFPT_PW::DFPT_PW() : pimpl_(new Impl()) {} @@ -464,14 +480,19 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { for (int ir = 0; ir < nrxx; ++ir) { v_sc_r[ir] = vh_r[ir]; } - if (xc_ != nullptr && !jprobe_noxc) { + const bool noxc = (getenv("DFPT_NOXC") != nullptr); + double xcs = 1.0; + if (getenv("DFPT_XCS") != nullptr) { + xcs = std::atof(getenv("DFPT_XCS")); + } + if (xc_ != nullptr && !jprobe_noxc && !noxc) { 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]; + v_sc_r[ir] += xcs * b_r[ir]; } } if (getenv("DFPT_MDBG") != nullptr) { @@ -875,6 +896,637 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { 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); + // DKCHK: analytic dk-derivative vs central difference of + // the GS-validated build_vkb (first atom with nh > 0 only) + if (getenv("DFPT_DKCHK") != nullptr && ia == 0 && ik == 0) { + const double dd = 1.0e-5; + std::vector> gk_p(npwk); + std::vector> gk_m(npwk); + for (int ig = 0; ig < npwk; ++ig) { + gk_p[ig] = gk[ig]; + gk_m[ig] = gk[ig]; + gk_p[ig][a] += dd; + gk_m[ig][a] -= dd; + } + std::vector>> vkb_p; + std::vector>> vkb_m; + pert_.build_vkb(it, ia, gk_p, vkb_p); + pert_.build_vkb(it, ia, gk_m, vkb_m); + for (int mu = 0; mu < nh; ++mu) { + std::complex ddot(0.0, 0.0); + double ndk = 0.0; + double nnum = 0.0; + for (int ig = 0; ig < npwk; ++ig) { + const std::complex num + = (vkb_p[mu][ig] - vkb_m[mu][ig]) / (2.0 * dd); + ddot += std::conj(dvkb[mu][ig]) * num; + ndk += std::norm(dvkb[mu][ig]); + nnum += std::norm(num); + } + std::cout << "DKCHK it=" << it << " a=" << a + << " mu=" << mu + << " =" << ddot + << " |dk|^2=" << ndk + << " |num|^2=" << nnum << std::endl; + } + } + // 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 + const bool ychk = (getenv("DFPT_YCHK") != nullptr); + 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; + } + if (ychk && npwk > 0) { + // eigen-projection identity: must equal the + // velocity-form matrix element -i / + // (tpiba (eps_m - eps_v)) for every nondegenerate + // conduction m (pos_matrix cross-check) + for (int m = 0; m < nbands; ++m) { + if (dfpt_band_occupied(wg_, ik, m)) { + continue; + } + const double de = eig_(ik, m) - eig_(ik, ib); + if (std::abs(de) < 1.0e-8) { + continue; + } + std::complex pdot(0.0, 0.0); + std::complex ydot(0.0, 0.0); + for (int ig = 0; ig < npwk; ++ig) { + pdot += std::conj(gs_psi_(ik, m, ig)) * vel[ib][ig]; + if (ig < static_cast(yvec[ik][ib].size())) { + ydot += std::conj(gs_psi_(ik, m, ig)) + * yvec[ik][ib][ig]; + } + } + const std::complex expect + = std::complex(0.0, -1.0) * pdot + / (tpiba * de); + std::cout << "YCHK a=" << a << " ik=" << ik + << " v=" << ib << " m=" << m + << " =(" << ydot.real() << "," << ydot.imag() << ")" + << " X_mv=(" << expect.real() << "," << expect.imag() << ")" + << " 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 + // zstar_eu cross-check (DFPT_ALEG probe). + 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) { + data_.set_current_iter(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_); + std::cout << "ALEG-E a=" << a << " iter=" << iter + << " residual=" << residual << 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::Impl::aleg_crosscheck(int q_idx) { + // zstar_eu cross-check (QE zstar_eu.f90): + // Z*(E,Us)_iat(a, d) = zion delta_ad + // - 2 sum_k w_k Re sum_v + // with the bare kappa leg from the same build_dv/apply_dv pair the + // displacement solves consumed. Printed against the plain (no star + // rotation) zstar_ue form from the dpsi_disp stash and the stored + // star-rotated Born charges, plus the SCF dielectric tensor + // eps = 1 - (16 pi / omega) sum_k wg Re + // which reduces to compute_eps at zero screening. A == B incriminates + // a shared operand; A clean incriminates the compute_born contraction. + const int nat = ucell_->nat; + const int nk = gs_psi_.get_nk(); + const int nbands = gs_psi_.get_nbands(); + std::vector>>>> de(3); + std::vector>>>> yr(3); + for (int a = 0; a < 3; ++a) { + de[a] = data_.get_dpsi_efield(a); + yr[a] = data_.get_pos_resp(a); + if (static_cast(de[a].size()) != nk + || static_cast(yr[a].size()) != nk) { + std::cout << "ALEG: missing E/position response stashes" << std::endl; + return; + } + } + // A-leg: plain k sum with the full wg weight (QE form). Complex + // accumulation: a spurious relative phase between the E and kappa legs + // cancels in the EE and kappa-kappa channels but rotates this cross + // channel, so Im(z) vs Re(z) is the phase detector (ASR: |z| = zion/2) + std::vector za(nat, ModuleBase::matrix(3, 3, true)); + std::vector>> za_c( + nat, std::vector>(9, std::complex(0.0, 0.0))); + for (int iat = 0; iat < nat; ++iat) { + for (int idir = 0; idir < 3; ++idir) { + pert_.build_dv(q_idx, iat, idir, data_); + for (int ik = 0; ik < nk; ++ik) { + if (static_cast(occ_kq_.size()) <= ik || occ_kq_[ik].empty()) { + continue; + } + pert_.apply_dv(q_idx, ik, gs_psi_, data_); + for (int v = 0; v < nbands; ++v) { + if (!dfpt_band_occupied(wg_, ik, v)) { + continue; + } + const std::vector> b + = data_.get_dpsi(q_idx, ik, v); + const int npw = static_cast(b.size()); + if (npw <= 0) { + continue; + } + for (int a = 0; a < 3; ++a) { + if (static_cast(de[a][ik][v].size()) != npw) { + continue; + } + std::complex dot(0.0, 0.0); + for (int ig = 0; ig < npw; ++ig) { + dot += std::conj(de[a][ik][v][ig]) * b[ig]; + } + za[iat](a, idir) += wg_(ik, v) * dot.real(); + za_c[iat][3 * a + idir] += wg_(ik, v) * dot; + if (a == 0 && idir == 0 && ik < 3) { + std::cout << "ALEG-K A ik=" << ik << " v=" << v + << " z=(" << dot.real() << "," << dot.imag() << ")" + << " wg=" << wg_(ik, v) << std::endl; + } + } + } + } + } + } + // plain B-leg from the displacement stashes (same sum, no stars) + std::vector zb(nat, ModuleBase::matrix(3, 3, true)); + for (int iat = 0; iat < nat; ++iat) { + for (int idir = 0; idir < 3; ++idir) { + 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; + } + const int npw = static_cast(disp[ik][v].size()); + if (npw <= 0) { + continue; + } + 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]; + } + zb[iat](a, idir) += wg_(ik, v) * dot.real(); + if (a == 0 && idir == 0 && ik < 3) { + std::cout << "ALEG-K B ik=" << ik << " v=" << v + << " z=(" << dot.real() << "," << dot.imag() << ")" + << " wg=" << wg_(ik, v) << std::endl; + } + } + } + } + } + } + // SCF dielectric tensor (complex accumulation: Im(chi) = phase detector) + ModuleBase::matrix eps(3, 3, true); + std::vector> eps_c(9, std::complex(0.0, 0.0)); + for (int ik = 0; ik < nk; ++ik) { + for (int v = 0; v < nbands; ++v) { + if (!dfpt_band_occupied(wg_, ik, v)) { + continue; + } + 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]; + } + eps(a, b) += wg_(ik, v) * dot.real(); + eps_c[3 * a + b] += wg_(ik, v) * dot; + } + } + } + } + // PTCROSS: bare cross-form operator diagnostic. A fresh bare E-leg + // solve x = M^-1(-Y^0) (rhs fully known, no screening) is contracted + // with the bare kappa leg b^kappa: must equal the spectral sum + // over the available empty bands -/(eps_m-eps_v); the + // mismatch beyond that band space measures what the truncated + // manifold misses in the cross channel that diagonal norm checks + // cannot see. The kappa-side solve x^kappa = M^-1(-b) gives the + // hermiticity mirror = conj(). + if (getenv("DFPT_PTCROSS") != nullptr) { + const ModuleBase::Vector3 q_cart_p + = data_.get_qvec(q_idx) * ucell_->G; + const int ikmax = std::min(nk, 3); + for (int idir = 0; idir < 3; ++idir) { + pert_.build_dv(q_idx, 0, idir, data_); + for (int ik = 0; ik < ikmax; ++ik) { + if (static_cast(occ_kq_.size()) <= ik || occ_kq_[ik].empty()) { + continue; + } + pert_.apply_dv(q_idx, ik, gs_psi_, data_); + if (last_q_ != q_idx || last_ik_ != ik) { + hamilt_->set_context(q_cart_p, ik); + last_q_ = q_idx; + last_ik_ = ik; + } + for (int v = 0; v < nbands; ++v) { + if (!dfpt_band_occupied(wg_, ik, v)) { + continue; + } + const int npw = static_cast(yr[0][ik][v].size()); + if (npw <= 0) { + continue; + } + const std::vector> b + = data_.get_dpsi(q_idx, ik, v); + if (static_cast(b.size()) != npw) { + continue; + } + // empty-band overlaps at this k + std::vector> myv; + std::vector> mbv; + std::vector dev; + double wsum = 0.0; + for (int m = 0; m < nbands; ++m) { + if (dfpt_band_occupied(wg_, ik, m)) { + continue; + } + const double de = eig_(ik, m) - eig_(ik, v); + if (std::abs(de) < 1.0e-8) { + continue; + } + std::complex my(0.0, 0.0); + std::complex mb(0.0, 0.0); + for (int i = 0; i < npw; ++i) { + my += std::conj(gs_psi_(ik, m, i)) * yr[0][ik][v][i]; + mb += std::conj(gs_psi_(ik, m, i)) * b[i]; + } + myv.push_back(my); + mbv.push_back(mb); + dev.push_back(de); + wsum += std::norm(my); + } + // E-side solve: (H - eps_v) x = -Y^0_v + std::vector> rhsE(npw); + for (int i = 0; i < npw; ++i) { + rhsE[i] = -yr[0][ik][v][i]; + } + hamilt_->set_shift(eig_(ik, v)); + std::vector> xE; + double resE = 0.0; + stern_.solve(*hamilt_, occ_kq_[ik], rhsE, + data_.get_max_iter(), data_.get_conv_thr(), + xE, resE); + // kappa-side solve: (H - eps_v) xk = -b + std::vector> rhsK(npw); + for (int i = 0; i < npw; ++i) { + rhsK[i] = -b[i]; + } + hamilt_->set_shift(eig_(ik, v)); + std::vector> xK; + double resK = 0.0; + stern_.solve(*hamilt_, occ_kq_[ik], rhsK, + data_.get_max_iter(), data_.get_conv_thr(), + xK, resK); + std::complex crossE(0.0, 0.0); + std::complex crossK(0.0, 0.0); + std::complex diagE(0.0, 0.0); + double pt_cross = 0.0; + double pt_diagE = 0.0; + double pt_diagK = 0.0; + for (int i = 0; i < npw; ++i) { + if (static_cast(xE.size()) == npw) { + crossE += std::conj(xE[i]) * b[i]; + diagE += std::conj(xE[i]) * yr[0][ik][v][i]; + } + if (static_cast(xK.size()) == npw) { + crossK += std::conj(xK[i]) * yr[0][ik][v][i]; + } + } + for (size_t im = 0; im < myv.size(); ++im) { + pt_cross += (-std::conj(myv[im]) * mbv[im] + / dev[im]).real(); + pt_diagE += -std::norm(myv[im]) / dev[im]; + pt_diagK += -std::norm(mbv[im]) / dev[im]; + } + std::cout << "PTCROSS d=" << idir << " ik=" << ik + << " v=" << v + << " crossE=(" << crossE.real() << "," + << crossE.imag() << ")" + << " crossK=(" << crossK.real() << "," + << crossK.imag() << ")" + << " pt=" << pt_cross + << " rE=" << (pt_cross != 0.0 + ? crossE.real() / pt_cross + : 0.0) + << " diagE=(" << diagE.real() << "," + << diagE.imag() << ") ptDiagE=" << pt_diagE + << " ptDiagK=" << pt_diagK + << " wsumY=" << wsum + << " resE=" << resE << " resK=" << resK + << std::endl; + } + } + } + } + for (int a = 0; a < 3; ++a) { + for (int b = 0; b < 3; ++b) { + eps(a, b) *= -16.0 * ModuleBase::PI / ucell_->omega; + if (a == b) { + eps(a, b) += 1.0; + } + } + } + std::cout << "ALEG eps_scf:" << std::endl; + for (int a = 0; a < 3; ++a) { + std::cout << " " << eps(a, 0) << " " << eps(a, 1) << " " << eps(a, 2) + << std::endl; + } + std::cout << "ALEG chi_scf complex diag: (16pi/omega)*z per component" + << std::endl; + for (int a = 0; a < 3; ++a) { + const std::complex z = eps_c[4 * a] * (-16.0 * ModuleBase::PI + / ucell_->omega); + std::cout << " a=" << a << " z=(" << z.real() << "," << z.imag() + << ") |z|=" << std::abs(z) << std::endl; + } + const ModuleBase::matrix eps_ipa = data_.get_dielectric(); + std::cout << "ALEG eps_ipa(stored):" << std::endl; + for (int a = 0; a < 3; ++a) { + std::cout << " " << eps_ipa(a, 0) << " " << eps_ipa(a, 1) << " " + << eps_ipa(a, 2) << std::endl; + } + for (int iat = 0; iat < nat; ++iat) { + const int it = ucell_->iat2it[iat]; + const double zion = ucell_->atoms[it].ncpp.zv; + std::cout << "ALEG atom " << iat << " zion=" << zion << std::endl; + std::cout << " A(eu plain):" << std::endl; + for (int a = 0; a < 3; ++a) { + std::cout << " "; + for (int d = 0; d < 3; ++d) { + std::cout << ((a == d) ? zion : 0.0) - 2.0 * za[iat](a, d) << " "; + } + std::cout << std::endl; + } + std::cout << " A complex diag: z00..z22 (sum wg)" << std::endl; + for (int a = 0; a < 3; ++a) { + const std::complex z = za_c[iat][4 * a]; + std::cout << " z" << a << a << " = (" << z.real() << "," + << z.imag() << ") |z|=" << std::abs(z) + << " |z|/(zion/2)=" << std::abs(z) / (0.5 * zion) + << std::endl; + } + std::cout << " B(ue plain):" << std::endl; + for (int a = 0; a < 3; ++a) { + std::cout << " "; + for (int d = 0; d < 3; ++d) { + std::cout << ((a == d) ? zion : 0.0) - 2.0 * zb[iat](a, d) << " "; + } + std::cout << std::endl; + } + const ModuleBase::matrix zstar = data_.get_born(iat); + std::cout << " B(ue star-rot stored):" << std::endl; + for (int a = 0; a < 3; ++a) { + std::cout << " " << zstar(a, 0) << " " << zstar(a, 1) << " " + << zstar(a, 2) << std::endl; + } + } +} + void DFPT_PW::run() { const int nq = pimpl_->qlist_.get_nq(); DFPT_IrrepData irrep_data(pimpl_->data_); @@ -884,15 +1536,16 @@ void DFPT_PW::run() { // 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_->data_.get_compute_q0()) { - pimpl_->q0_.compute_q0_response(pimpl_->data_); - if (pimpl_->wired()) { - // the dielectric tensor and Born charges of the C6 velocity - // form; the LO-TO term below consumes them - pimpl_->q0_.compute_eps(pimpl_->gs_psi_, pimpl_->wg_, pimpl_->eig_, pimpl_->data_); - pimpl_->q0_.compute_born(pimpl_->gs_psi_, pimpl_->wg_, pimpl_->eig_, pimpl_->data_); + if (q_idx == 0 && pimpl_->data_.get_compute_q0()) { + pimpl_->q0_.compute_q0_response(pimpl_->data_); + if (pimpl_->wired()) { + // the dielectric tensor of the C6 velocity form is a + // ground-state quantity; the Born charges below need the + // converged q = 0 Sternheimer solutions and run after the + // two-pass displacement solves + pimpl_->q0_.compute_eps(pimpl_->gs_psi_, pimpl_->wg_, pimpl_->eig_, pimpl_->data_); + } } - } // occupied states at k+q for every k of this q (projector of P_c); // also invalidates the shifted-operator context cache @@ -900,6 +1553,19 @@ void DFPT_PW::run() { 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); + // E-field SCF responses for the zstar_eu cross-check probe: + // after the bare Y legs they consume, before the displacement + // solves reuse the slots (DFPT_ALEG only) + if (getenv("DFPT_ALEG") != nullptr) { + pimpl_->solve_efield_resp(q_idx); + } + } + // 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. @@ -941,6 +1607,18 @@ void DFPT_PW::run() { } } + // 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_); + if (getenv("DFPT_ALEG") != nullptr) { + pimpl_->aleg_crosscheck(q_idx); + } + } + pimpl_->phon_.assemble(q_idx, pimpl_->data_); pimpl_->phon_.diagonalize(q_idx, pimpl_->data_); if (q_idx == 0 && pimpl_->data_.get_loto()) { diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index 18c48fbba90..4e965f317c5 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -101,6 +101,44 @@ DFPT_PW_Data::get_dpsi_disp(int atom_idx, int dir) const { 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]; diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index 0507c95b37a..e89b0dced6c 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -157,7 +157,26 @@ class DFPT_PW_Data { 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; @@ -202,6 +221,14 @@ class DFPT_PW_Data { /// 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; diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index 6aeeb52039c..423ae54b8af 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -418,7 +418,11 @@ void DFPT_Q0::compute_eps(const psi::Psi>& psi, } for (int a = 0; a < 3; ++a) { for (int b = 0; b < 3; ++b) { - eps(a, b) *= 8.0 * ModuleBase::PI / ucell_->omega; + // 16 pi / Omega: QE dielec.f90 form eps = 1 - 4*(4pi/Omega)*wk* + // Re; the PT r-matrix form carries the same factor + // through = and the Re pairing (validated against + // QE 7.2 Si to 0.05%: 23.68 here vs 23.67 QE) + eps(a, b) *= 16.0 * ModuleBase::PI / ucell_->omega; if (a == b) { eps(a, b) += 1.0; } @@ -430,22 +434,22 @@ void DFPT_Q0::compute_eps(const psi::Psi>& psi, void DFPT_Q0::compute_born(const psi::Psi>& psi, const ModuleBase::matrix& wg, const ModuleBase::matrix& eig, DFPT_PW_Data& data) { - if (ucell_ == nullptr || pert_ == nullptr) { + if (ucell_ == nullptr) { return; } - const bool zdbg = getenv("DFPT_ZDBG") != nullptr; - std::vector>>>> r_mat; - pos_matrix(psi, eig, r_mat); 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; - // stash the q=0 dpsi slots (apply_dv reuses them, phon backup pattern) - std::vector>>> dpsib(nk); - for (int ik = 0; ik < nk; ++ik) { - dpsib[ik].resize(nbands); - for (int ib = 0; ib < nbands; ++ib) { - dpsib[ik][ib] = data.get_dpsi(0, ik, ib); + // 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 } } @@ -458,45 +462,32 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, // 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) { - // dV matrix elements at q = 0 through the C1 path; apply_dv - // delivers dV|u_v> on the k+q = k basis for every k. - pert_->build_dv(0, iat, idir, data); + // 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) { - pert_->apply_dv(0, ik, psi, data); for (int v = 0; v < nbands; ++v) { if (!dfpt_band_occupied(wg, ik, v)) { continue; // empty } - const std::vector> rhs = - data.get_dpsi(0, ik, v); - if (rhs.empty()) { - continue; + const int npw = static_cast(disp[ik][v].size()); + if (npw <= 0 || npw > nbasis) { + continue; // unsolved slot or inconsistent basis } - for (int m = 0; m < nbands; ++m) { - const double de = eig(ik, m) - eig(ik, v); - if (std::abs(de) < 1.0e-8) { - continue; // m == v or degenerate partner - } - std::complex dv_mv(0.0, 0.0); - for (size_t ig = 0; ig < rhs.size(); ++ig) { - dv_mv += std::conj(psi(ik, m, ig)) * rhs[ig]; + // per field direction + for (int a = 0; a < 3; ++a) { + if (static_cast(yr[a][ik][v].size()) != npw) { + continue; } - if (zdbg) { - const ModuleBase::Vector3>& ra - = r_mat[ik][m][v]; - std::cout << "ZDBG iat=" << iat << " idir=" << idir - << " ik=" << ik << " v=" << v << " m=" << m - << " wg=" << wg(ik, v) << " de=" << de - << " dv=" << dv_mv << " r=" << ra - << std::endl; - } - // = conj(dv_mv), multiplied from the - // right by (Gonze-Lee ordering) - for (int a = 0; a < 3; ++a) { - chi_k[ik](a, idir) - += wg(ik, v) - * (std::conj(dv_mv) * r_mat[ik][m][v][a]).real() / de; + 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(); } } } @@ -525,7 +516,7 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, ModuleBase::matrix zstar(3, 3, true); for (int a = 0; a < 3; ++a) { for (int d = 0; d < 3; ++d) { - zstar(a, d) = -4.0 * zacc[iat](a, d); + zstar(a, d) = -2.0 * zacc[iat](a, d); } } // ionic rigid-ion charge on the diagonal (a == b directions) @@ -536,15 +527,6 @@ void DFPT_Q0::compute_born(const psi::Psi>& psi, } data.set_born(iat, zstar); } - - // restore the stashed q=0 dpsi - for (int ik = 0; ik < nk; ++ik) { - for (int ib = 0; ib < nbands; ++ib) { - if (!dpsib[ik][ib].empty()) { - data.set_dpsi(0, ik, ib, dpsib[ik][ib]); - } - } - } } void DFPT_Q0::compute_q0_response(DFPT_PW_Data& data) { diff --git a/source/source_pw/module_dfpt/dfpt_q0.h b/source/source_pw/module_dfpt/dfpt_q0.h index 2c96fd56c53..3f3ffefb30b 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.h +++ b/source/source_pw/module_dfpt/dfpt_q0.h @@ -38,18 +38,34 @@ class DFPT_Pert; * 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 (King-Smith/Resta Berry phases; the m sum runs - * over ALL bands, occupied and empty, m != v): - * Z*_k,ab = Z_k delta_ab - 4 sum_{k,v occ,m!=v} wg - * * Re[] / (eps_m - 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 bare displacement potential dV/dtau comes from DFPT_Pert (C1) at - * q = 0; the absolute calibration of both expressions is pinned by the - * diamond end-to-end test in C7 (structure/symmetry by the C6 tests). + * 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: 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 index 3aaf734004d..86f94b56ea2 100644 --- 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 @@ -93,7 +93,7 @@ Structure_Factor::~Structure_Factor() * 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 full prefactor chain (8 pi / Omega, wg, + * - DFPT_Q0::compute_eps: the full prefactor chain (16 pi / Omega, wg, * 1/(eps_c - eps_v)) on a two-level toy system with a complex excited * state. * - DFPT_Q0::compute_born: elementwise against the closed-form @@ -621,7 +621,7 @@ TEST_F(DFPTQ0SerialTest, ComputeEpsTwoLevelAnalytic) = std::complex(0.0, -1.0) * std::conj(p01[b]) / (ucell_.tpiba * (eig(0, 1) - eig(0, 0))); const double expect = ((a == b) ? 1.0 : 0.0) - + 8.0 * ModuleBase::PI / ucell_.omega * wg(0, 0) + + 16.0 * ModuleBase::PI / ucell_.omega * wg(0, 0) * (r_vc * r_cv).real() / de; EXPECT_NEAR(eps(a, b), expect, 1.0e-10) << "a=" << a << " b=" << b; } @@ -629,18 +629,18 @@ TEST_F(DFPTQ0SerialTest, ComputeEpsTwoLevelAnalytic) } // --------------------------------------------------------------------------- -// compute_born against the closed-form q = 0 sums (Coulomb local dV) +// 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, ComputeBornAnalyticCoulomb) +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); - 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_); psi::Psi> psi(1, 2, npwk, npwk, true); @@ -648,8 +648,6 @@ TEST_F(DFPTQ0SerialTest, ComputeBornAnalyticCoulomb) psi(0, 0, ig0) = std::sqrt(0.6); psi(0, 0, igx) = std::sqrt(0.4); psi(0, 1, ig0) = std::sqrt(0.2); - psi(0, 1, igx) = -std::sqrt(0.3); - psi(0, 1, igy) = std::complex(0.0, std::sqrt(0.5)); ModuleBase::matrix wg(1, 2); wg(0, 0) = 2.0; @@ -658,56 +656,66 @@ TEST_F(DFPTQ0SerialTest, ComputeBornAnalyticCoulomb) eig(0, 0) = 0.4 * e1; eig(0, 1) = e1; - // sentinel dpsi in the q = 0 slot: compute_born must restore it + // 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); - // closed-form dV matrix elements: supp(v) = {G0, Gx}, supp(m) = {G0, Gx, Gy} - const std::complex cv[3] = {psi(0, 0, ig0), psi(0, 0, igx), std::complex(0.0, 0.0)}; - const ModuleBase::Vector3 gv[3] = {ModuleBase::Vector3(0.0, 0.0, 0.0), gx_, - gy_}; - const std::complex cm[3] = {psi(0, 1, ig0), psi(0, 1, igx), psi(0, 1, igy)}; - const double de = eig(0, 1) - eig(0, 0); + const double zion = ucell_.atoms[0].ncpp.zv; for (int idir = 0; idir < 3; ++idir) { - // dv_m0 = = sum_{G'' in supp(m)} cc_m(G'') - // sum_{G' in supp(v)} c_v(G') AnalyticDVloc(G'' - G') - std::complex dv10(0.0, 0.0); - for (int im = 0; im < 3; ++im) - { - if (cm[im] == std::complex(0.0, 0.0)) - { - continue; - } - for (int iv = 0; iv < 2; ++iv) - { - dv10 += std::conj(cm[im]) * cv[iv] * AnalyticDVloc(idir, gv[im] - gv[iv]); - } - } for (int a = 0; a < 3; ++a) { - // p_01^a = 2 tpiba^2 , kinetic operator diagonal in G: - // only shared components pair (G0 = 0 drops out, v has no Gy) - std::complex p01_a(0.0, 0.0); - for (int g = 0; g < 3; ++g) - { - p01_a += 2.0 * ucell_.tpiba2 * std::conj(cv[g]) * gv[g][a] * cm[g]; - } - const std::complex r_10 - = std::complex(0.0, -1.0) * std::conj(p01_a) - / (ucell_.tpiba * (eig(0, 1) - eig(0, 0))); - // ionic Z sits on the (a == idir) diagonal only - const double zion = (a == idir) ? ucell_.atoms[0].ncpp.zv : 0.0; - const double expect - = zion - 4.0 * wg(0, 0) * (std::conj(dv10) * r_10).real() / de; - EXPECT_NEAR(zstar(a, idir), expect, 1.0e-9) << "a=" << a << " idir=" << idir; + // = 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 restored + // 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) From 98c7f114c7a77483b7e445cafef81cfe5cd09f8b Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Thu, 20 Aug 2026 20:06:33 +0800 Subject: [PATCH 31/50] DFPT q0: promote the E-field SCF solve, compute_eps to the dielec.f90 screened form - solve_efield_resp is now production (QE solve_e order): runs after solve_pos_resp, before the displacement solves; converged dpsi^E,a stashed through DFPT_PW_Data (dpsi_efield) - compute_eps consumes pos_resp + dpsi_efield: eps = 1 - (16 pi/Omega) sum_k wg sum_occ Re, star-rotated on symmetry-reduced meshes; the PT r-matrix path is retired (pos_matrix kept as the design-phase analytic reference for its serial tests) - serial test ComputeEpsScfSyntheticStash replaces the PT two-level case (prefactor, wg, occupied sum, conj/index pinning, empty-row skip); 6/6 - end-to-end sym 4x4x4: eps = 23.35 delta (was IPA 12.67), consistent with the nosym ALEG value 23.68 and QE dielec.f90 anchor 23.67 --- source/source_pw/module_dfpt/dfpt_pw.cpp | 32 +++---- source/source_pw/module_dfpt/dfpt_q0.cpp | 56 +++++++----- source/source_pw/module_dfpt/dfpt_q0.h | 12 ++- .../test_serial/dfpt_q0_serial_test.cpp | 91 ++++++++++--------- 4 files changed, 107 insertions(+), 84 deletions(-) diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index b50c0aa44cf..71a3b04c6d8 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -1110,7 +1110,8 @@ void DFPT_PW::Impl::solve_efield_resp(int q_idx) { // 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 - // zstar_eu cross-check (DFPT_ALEG probe). + // SCF dielectric tensor (DFPT_Q0::compute_eps) and the zstar_eu + // cross-check probe (DFPT_ALEG). if (!wired() || hamilt_ == nullptr) { return; } @@ -1190,8 +1191,11 @@ void DFPT_PW::Impl::solve_efield_resp(int q_idx) { rho_.mix_drho(q_idx, data_); const double residual = rho_.get_residual(q_idx, data_); converged = (residual < conv_thr_); - std::cout << "ALEG-E a=" << a << " iter=" << iter - << " residual=" << residual << std::endl; + 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( @@ -1214,7 +1218,8 @@ void DFPT_PW::Impl::aleg_crosscheck(int q_idx) { // rotation) zstar_ue form from the dpsi_disp stash and the stored // star-rotated Born charges, plus the SCF dielectric tensor // eps = 1 - (16 pi / omega) sum_k wg Re - // which reduces to compute_eps at zero screening. A == B incriminates + // which is the same contraction compute_eps produces in production. + // A == B incriminates // a shared operand; A clean incriminates the compute_born contraction. const int nat = ucell_->nat; const int nk = gs_psi_.get_nk(); @@ -1538,13 +1543,6 @@ void DFPT_PW::run() { // commutator [Ĥ_SCF, r̂]. This is implemented in DFPT_Q0 module. if (q_idx == 0 && pimpl_->data_.get_compute_q0()) { pimpl_->q0_.compute_q0_response(pimpl_->data_); - if (pimpl_->wired()) { - // the dielectric tensor of the C6 velocity form is a - // ground-state quantity; the Born charges below need the - // converged q = 0 Sternheimer solutions and run after the - // two-pass displacement solves - pimpl_->q0_.compute_eps(pimpl_->gs_psi_, pimpl_->wg_, pimpl_->eig_, pimpl_->data_); - } } // occupied states at k+q for every k of this q (projector of P_c); @@ -1558,12 +1556,12 @@ void DFPT_PW::run() { // 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); - // E-field SCF responses for the zstar_eu cross-check probe: - // after the bare Y legs they consume, before the displacement - // solves reuse the slots (DFPT_ALEG only) - if (getenv("DFPT_ALEG") != nullptr) { - pimpl_->solve_efield_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 diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index 423ae54b8af..af13884bea2 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -364,38 +364,49 @@ void DFPT_Q0::pos_matrix(const psi::Psi>& psi, } } -void DFPT_Q0::compute_eps(const psi::Psi>& psi, - const ModuleBase::matrix& wg, - const ModuleBase::matrix& eig, DFPT_PW_Data& data) { +void DFPT_Q0::compute_eps(const ModuleBase::matrix& wg, DFPT_PW_Data& data) { if (ucell_ == nullptr) { return; } - std::vector>>>> r_mat; - pos_matrix(psi, eig, r_mat); - const int nk = psi.get_nk(); - const int nbands = psi.get_nbands(); + 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 susceptibility chi_k[ik](a, b) at every stored k + // 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 c = 0; c < nbands; ++c) { - if (dfpt_band_occupied(wg, ik, c)) { - continue; // occupied - } - const double de = eig(ik, c) - eig(ik, v); - if (std::abs(de) < 1.0e-8) { + for (int a = 0; a < 3; ++a) { + const int npw = static_cast(yr[a][ik][v].size()); + if (npw <= 0) { continue; } - for (int a = 0; a < 3; ++a) { - for (int b = 0; b < 3; ++b) { - chi_k[ik](a, b) - += wg(ik, v) - * (r_mat[ik][v][c][a] * r_mat[ik][c][v][b]).real() / de; + 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(); } } } @@ -419,10 +430,9 @@ void DFPT_Q0::compute_eps(const psi::Psi>& psi, 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; the PT r-matrix form carries the same factor - // through = and the Re pairing (validated against - // QE 7.2 Si to 0.05%: 23.68 here vs 23.67 QE) - eps(a, b) *= 16.0 * ModuleBase::PI / ucell_->omega; + // 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; } diff --git a/source/source_pw/module_dfpt/dfpt_q0.h b/source/source_pw/module_dfpt/dfpt_q0.h index 3f3ffefb30b..3b78db34f25 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.h +++ b/source/source_pw/module_dfpt/dfpt_q0.h @@ -75,9 +75,12 @@ class DFPT_Q0 { void init(UnitCell& ucell, ModulePW::PW_Basis* pw_rho, ModulePW::PW_Basis_K* pw_wfc, DFPT_Pert* pert); - void compute_eps(const psi::Psi>& psi, - const ModuleBase::matrix& wg, - const ModuleBase::matrix& eig, DFPT_PW_Data& data); + /// 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, @@ -88,6 +91,9 @@ class DFPT_Q0 { /// 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); 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 index 86f94b56ea2..6163f8054b9 100644 --- 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 @@ -93,9 +93,10 @@ Structure_Factor::~Structure_Factor() * 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 full prefactor chain (16 pi / Omega, wg, - * 1/(eps_c - eps_v)) on a two-level toy system with a complex excited - * state. + * - 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. @@ -565,65 +566,73 @@ TEST_F(DFPTQ0SerialTest, PosMatrixNonlocalMatchesOperatorFiniteDifference) } // --------------------------------------------------------------------------- -// compute_eps on a two-level system with a complex excited state +// 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, ComputeEpsTwoLevelAnalytic) +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); - 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_); - // v = sqrt(0.6)|G0> + sqrt(0.4)|Gx> (eps = 0.4 e1) - // c = sqrt(0.2)|G0> - sqrt(0.3)|Gx> + i sqrt(0.5)|Gy> (eps = e1) - // (the relative i phase keeps the reference sensitive to conj placement) - 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); - psi(0, 1, igx) = -std::sqrt(0.3); - psi(0, 1, igy) = std::complex(0.0, std::sqrt(0.5)); 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; - q0_.compute_eps(psi, wg, eig, data_); - const ModuleBase::matrix eps = data_.get_dielectric(); + // 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); + } - // closed-form velocity elements between the two states (kinetic operator - // diagonal in G: G0 pairs G0 with G = 0, the Gy component of c pairs - // with the vanishing Gy component of v) - std::complex p01[3]; - for (int d = 0; d < 3; ++d) + // 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) { - p01[d] = 2.0 * ucell_.tpiba2 - * (std::sqrt(0.6) * std::sqrt(0.2) * 0.0 - + std::sqrt(0.4) * (-std::sqrt(0.3)) * gx_[d]); + 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); } - const double de = eig(0, 1) - eig(0, 0); + + 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) { - const std::complex r_vc - = std::complex(0.0, -1.0) * p01[a] / (ucell_.tpiba * (eig(0, 0) - eig(0, 1))); - const std::complex r_cv - = std::complex(0.0, -1.0) * std::conj(p01[b]) - / (ucell_.tpiba * (eig(0, 1) - eig(0, 0))); + // = 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) - * (r_vc * r_cv).real() / de; - EXPECT_NEAR(eps(a, b), expect, 1.0e-10) << "a=" << a << " b=" << b; + - 16.0 * ModuleBase::PI / ucell_.omega + * wg(0, 0) * dot.real(); + EXPECT_NEAR(eps(a, b), expect, 1.0e-12) << "a=" << a << " b=" << b; } } } From be3e5ee8010314950bb5e801be8014a4249b1fae Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Thu, 20 Aug 2026 21:37:33 +0800 Subject: [PATCH 32/50] DFPT: build_occ_kq diagnostic detail in the commensurability error; PLAN P0-3 intake (non-Gamma-q chain defect, eps SCF promotion record) --- .../module_dfpt/PLAN_dfpt_implementation.md | 32 +++++++++++++++++++ source/source_pw/module_dfpt/dfpt_pw.cpp | 13 +++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 1f51cbc6ff7..9901a482c59 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -435,6 +435,38 @@ (dfpt_pw_data)随 ALEG 保留 - [ ] P0-3 B0 收尾:8×8×8 过夜(ε∞/Z*/D 密网格收敛);非 Γ q 物理级验证 (密 k 色散 vs 超胞 FD);sym1 星旋转各向异性处理或记录在案 + - **compute_eps SCF 化(完成,98c7f114c)**:solve_efield_resp + 转正(QE solve_e 顺序:Y 腿后、位移 solve 前); + compute_eps 改消耗 pos_resp+dpsi_efield 收缩 + ε=δ−(16π/Ω)Σwg Σ_occ Re⟨Y^a|dψ^E,b⟩(dielec.f90 锚,星平均 + 保留);PT r-matrix 路径退役(pos_matrix 保留为解析参照); + ComputeEpsScfSyntheticStash 替换 PT 用例,串行 6/6; + 端到端 sym 4×4×4 ε∞=23.35·δ(原 IPA 12.67),与 nosym + ALEG 23.68、QE 23.67 同源 + - **[新缺陷登记] 非 Γ q 全链路错误(P0-3 阻塞项)**: + QE 7.2 本地参照(同 UPF/胞/ecut/4×4×4 网格): + q=L(0.5,0,0) → 125.39×2/239.10/473.79×2/496.34 cm⁻¹; + q=Γ-L 1/4 → 102.75×2/144.31/494.62×2/502.63。我们: + L 点 2-k 显式 → −1170.9/−296.2×2/292.5/560.5×2(简并 + 结构 1+2+1+2 正确、幅度全错);L 点 64k(sym−1) → + −948/−148×2/183/199×2;q=1/4 64k → −1474/−1038×2/ + −957×2/−954(全虚频)。**早期 b0_si_qL"验证"结论作废** + (当时无外部参照,q↔−q 0.2-1% 一致本身即误差信号) + - 已排除:屏蔽装配(DFPT_NOSC 下 2-k bare 也错 −3291); + H(k+q) 组装(DBG ⟨ψ|H(k+q)|ψ⟩=GS eig 到 2e-6); + apply_vr_core 卷积约定(PW_Basis_K 实空间为周期 u_k, + 纯 G 卷积 + e^{i(Δ+q)τ} 相位数学正确);DFPT_KQ_Basis 球 + 选择(与 GS 同球一致);d2ionq Ewald(有 FD 验证注释+用例) + - 存活嫌疑:dVloc_dtau(dVnl_dtau) 的 (Δ+q) 系数链在真实 + case 的实现(C1 fixture 测试过的约定可能未覆盖全网格 + dn≠0 标签折叠路径);build_occ_kq 的 dn≠0 G 向量匹配 + (2-k case dn=(1,0,0) 当时验证过,64k 大量 dn≠0 未验); + term2 cross 的逐元素正确性(DFPT_XB 打印未对照) + - 下一步(调试战役):① XB 逐元素 + 手算矩阵元对照 + (2-k case 最小复现);② 独立 python FD 实现 dV_loc(q) + 系数在真实 gcar 网格上对照;③ 超胞 FD 声子端到端参照 + (4×1×1 胞 Γ 点,q=1/4 对应) + - 8×8×8 nosym ALEG 验收运行中(~4h,q=0 不受本缺陷影响) - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` - dfpt_qfile + QList::read_from_file 端到端首次运行:q=(0.5,0,0),k={Γ,L}, compute_q0=false/loto=false;6 位移全收敛、无 NaN、D Hermitian diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 71a3b04c6d8..3062392088e 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -244,10 +244,15 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { } } if (ikq < 0) { - ModuleBase::WARNING_QUIT("DFPT_PW::build_occ_kq", - "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)."); + 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; From a915352cd47e56bfb54d471ba20299d1698f3c55 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 24 Aug 2026 19:31:35 +0800 Subject: [PATCH 33/50] DFPT: fix q!=Gamma phonon frequencies (missing spin factor 2 in drho), KQ dual-reservoir completeness, term3 d2 ungating - compute_drho: include the spin factor 2 at every q (QE incdrhoscf wgt = 2*weight/omega); the q=0 Hermitian completion now keeps Re only instead of 2 Re. Previously the screening was half strength away from Gamma, which collapsed the L-point Si frequencies to -948/-148/182/199 cm^-1. After the fix: 100.49/100.49/380.41/402.11/485.93/485.93 cm^-1 vs QE 101.61x2/380.54/402.24/486.28x2 (Si NC 4x4x4, 0.1-1.1%); Gamma stays 517.491 cm^-1 (QE 517.633). - dfpt_kq_basis: dual-reservoir G assembly so the k and k+q balls share the same igl2ig maps (fixes silent truncation when one ball exhausts the rho-grid reservoir). - dfpt_phon: drop the 2q-reciprocal gate on the same-atom d2 term (it is q-independent by construction; the old gate silently dropped it and produced imaginary branches). - Verification: ctest 12/12 (MODULE_CELL x4 + MODULE_DFPT x8); serial 4/4 (pert/phon/q0/rho); bare-response L run matches QE niter_ph=1 to 0.008-0.4% (-2281.83 vs -2282.01 etc.). - No docs change: module_dfpt is design-phase, no INPUT parameter touched. --- .../module_dfpt/dfpt_hamilt_shift.cpp | 23 +- .../source_pw/module_dfpt/dfpt_kq_basis.cpp | 119 +++++-- source/source_pw/module_dfpt/dfpt_kq_basis.h | 52 +-- source/source_pw/module_dfpt/dfpt_pert.cpp | 85 ++--- source/source_pw/module_dfpt/dfpt_pert.h | 1 + source/source_pw/module_dfpt/dfpt_phon.cpp | 65 +--- source/source_pw/module_dfpt/dfpt_pw.cpp | 2 +- source/source_pw/module_dfpt/dfpt_rho.cpp | 63 ++-- .../module_dfpt/test/dfpt_kq_basis_test.cpp | 303 ++++++++++-------- .../test_serial/dfpt_pert_serial_test.cpp | 4 +- .../test_serial/dfpt_phon_serial_test.cpp | 8 +- .../test_serial/dfpt_rho_serial_test.cpp | 6 +- 12 files changed, 371 insertions(+), 360 deletions(-) diff --git a/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp b/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp index 51ec2610086..3321ee541e7 100644 --- a/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp +++ b/source/source_pw/module_dfpt/dfpt_hamilt_shift.cpp @@ -63,31 +63,14 @@ DFPT_HamiltShift::DFPT_HamiltShift(const UnitCell& ucell, DFPT_HamiltShift::~DFPT_HamiltShift() {} void DFPT_HamiltShift::set_context(const ModuleBase::Vector3& q_cart, int k_idx) { - kq_.init(pw_wfc_, q_cart, k_idx); + kq_.init(pw_wfc_, pw_rho_, q_cart, k_idx); ik_cache_ = k_idx; const int npw = kq_.get_npwk(); - // rho ig -> shared FFT-cell reverse map, then k+q -> rho through the - // (ix,iy,iz) triple (the stick encodings of the two bases differ) - 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; - } + // 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) { - const int isz = kq_.get_ig2isz(igl); - 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; - kq2rho_[igl] = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + kq2rho_[igl] = kq_.get_ig_rho(igl); } // cache the beta projectors of every atom on the k+q list diff --git a/source/source_pw/module_dfpt/dfpt_kq_basis.cpp b/source/source_pw/module_dfpt/dfpt_kq_basis.cpp index 892d474fbff..143c813afc9 100644 --- a/source/source_pw/module_dfpt/dfpt_kq_basis.cpp +++ b/source/source_pw/module_dfpt/dfpt_kq_basis.cpp @@ -1,31 +1,39 @@ // ============================================================ // 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 +// 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; - igl2ig_.clear(); + ig_rho_.clear(); gk2_.clear(); gcar_.clear(); - if (pw_wfc_ == nullptr) + if (pw_wfc_ == nullptr || pw_rho == nullptr) { return; } @@ -40,45 +48,95 @@ void DFPT_KQ_Basis::init(const ModulePW::PW_Basis_K* pw_wfc, "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; - // Reuse the ground-state k-basis G grid (shared by all k points of the - // pool): the k+q ball is a subset of it for every ik and q (see the - // class documentation), so only the shifted-center selection is needed. - const int npw = pw_wfc_->npw; - for (int ig = 0; ig < npw; ++ig) + // 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_wfc_->ig2isz[ig]; - int iz = isz % pw_wfc_->nz; - const int is = isz / pw_wfc_->nz; - const int ixy = pw_wfc_->is2fftixy[is]; - int ix = ixy / pw_wfc_->fftny; - int iy = ixy % pw_wfc_->fftny; - if (ix >= int(pw_wfc_->nx / 2) + 1) + 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 >= int(pw_wfc_->ny / 2) + 1) + if (iy >= static_cast(pw_wfc_->ny / 2) + 1) { iy -= pw_wfc_->ny; } - if (iz >= int(pw_wfc_->nz / 2) + 1) + if (iz >= static_cast(pw_wfc_->nz / 2) + 1) { iz -= pw_wfc_->nz; } - ModuleBase::Vector3 f(ix, iy, iz); - const ModuleBase::Vector3 gcar = f * pw_wfc_->G; + 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) + if (gk2 > pw_wfc_->gk_ecut) { - igl2ig_.push_back(ig); - gcar_.push_back(gcar); - gk2_.push_back(gk2); + 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(igl2ig_.size()); + npwk_ = static_cast(gcar_.size()); } void DFPT_KQ_Basis::clear() @@ -86,14 +144,9 @@ void DFPT_KQ_Basis::clear() pw_wfc_ = nullptr; kplusq_c_ = ModuleBase::Vector3(); npwk_ = 0; - igl2ig_.clear(); + ig_rho_.clear(); gk2_.clear(); gcar_.clear(); } -int DFPT_KQ_Basis::get_ig2isz(int igl) const -{ - return pw_wfc_->ig2isz[igl2ig_[igl]]; -} - -} // namespace ModuleDFPT \ No newline at end of file +} // namespace ModuleDFPT diff --git a/source/source_pw/module_dfpt/dfpt_kq_basis.h b/source/source_pw/module_dfpt/dfpt_kq_basis.h index cb5cd07b54b..7e26307c742 100644 --- a/source/source_pw/module_dfpt/dfpt_kq_basis.h +++ b/source/source_pw/module_dfpt/dfpt_kq_basis.h @@ -1,8 +1,10 @@ // ============================================================ // 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 +// 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. // ============================================================ @@ -13,6 +15,7 @@ #include namespace ModulePW { +class PW_Basis; class PW_Basis_K; } @@ -24,21 +27,22 @@ namespace ModuleDFPT { * 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 - * re-filters the G vectors of an already-initialized ground-state k-basis - * PW_Basis_K at the shifted center k+q. Each G with |G + (k+q)|^2 <= gk_ecut - * satisfies |G| <= sqrt(gk_ecut) + |k+q|, which is within the FFT-grid ball - * (ggecut) the ground-state basis already distributed, so no G vector needed - * by k+q is missing and only the shifted-center selection is performed. + * 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 basis must be a complex (gamma_only=false) k-basis: - * DFPT couples k and k+q symmetrically and the q-perturbation breaks the + * - 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. - * - Every G vector needed by the largest k+q ball must lie inside the FFT - * grid of the ground-state basis, i.e. the FFT-grid cutoff (gridecut_lat) - * must satisfy gridecut_lat >= (sqrt(gk_ecut) + max|k| + max|q|)^2. - * In practice ecutrho >= 4*ecutwfc covers every q inside the first - * Brillouin zone, which is the DFPT q range. + * - 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: @@ -48,10 +52,12 @@ class 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); @@ -60,10 +66,9 @@ class DFPT_KQ_Basis { 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 underlying ground-state G vector - int get_ig(int igl) const { return igl2ig_[igl]; } - ///< slab index (ig2isz) of the underlying ground-state G vector - int get_ig2isz(int igl) const; + ///< 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 @@ -72,7 +77,6 @@ class DFPT_KQ_Basis { 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_igl2ig() const { return igl2ig_; } const std::vector& get_gk2_all() const { return gk2_; } const std::vector>& get_gcar_all() const { return gcar_; } @@ -80,11 +84,11 @@ class DFPT_KQ_Basis { 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 igl2ig_; ///< local k+q index -> base G index - std::vector gk2_; ///< |G + (k+q)|^2 - std::vector> gcar_; ///< G in Cartesian coordinates + 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 \ No newline at end of file +#endif // DFPT_KQ_BASIS_H diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index ecb7f5dcd3f..8bb071a0b3a 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -18,6 +18,9 @@ #include "source_pw/module_pwdft/stru_fac.h" #include +#include +#include +#include namespace ModuleDFPT { @@ -188,7 +191,7 @@ void DFPT_Pert::apply_vr(int q_idx, int k_idx, return; } DFPT_KQ_Basis kq; - kq.init(pw_wfc_, q_cart, k_idx); + kq.init(pw_wfc_, pw_rho_, q_cart, k_idx); apply_vr_core(k_idx, v_rc, psi, kq, dv_psi); } @@ -197,20 +200,6 @@ void DFPT_Pert::apply_vr_core(int k_idx, const psi::Psi>& psi, const DFPT_KQ_Basis& kq, std::vector>>& dv_psi) const { - // Invert both ig -> FFT-cell mappings through the (ix,iy,iz) triple: the - // rho and wfc bases enumerate different G balls, so their isz encodings - // (stick tables) are not interchangeable - only the FFT cell position of - // a plane wave is shared between the two bases. - 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; - } const int nbands = psi.get_nbands(); const int npwk_kq = kq.get_npwk(); std::vector> u_r(pw_rho_->nrxx); @@ -225,20 +214,13 @@ void DFPT_Pert::apply_vr_core(int k_idx, 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) { - // kq isz uses the wfc stick tables - const int isz = kq.get_ig2isz(igl); - 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; - const int ig_rho = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + const int ig_rho = kq.get_ig_rho(igl); if (ig_rho >= 0) { dpsi[igl] = d_recip[ig_rho]; } } - dv_psi[iband] = dpsi; - } + dv_psi[iband] = dpsi; + } } void DFPT_Pert::apply_dv(int q_idx, int k_idx, const psi::Psi>& psi, @@ -248,7 +230,7 @@ void DFPT_Pert::apply_dv(int q_idx, int k_idx, const psi::Psi q_cart = data.get_qvec(q_idx) * ucell_->G; DFPT_KQ_Basis kq; - kq.init(pw_wfc_, q_cart, k_idx); + kq.init(pw_wfc_, pw_rho_, q_cart, k_idx); const int nbands = psi.get_nbands(); std::vector>> dv_psi(nbands); @@ -259,33 +241,6 @@ void DFPT_Pert::apply_dv(int q_idx, int k_idx, const psi::Psi (per displaced atom) std::vector>> dv_psi_nl; dVnl_dtau(atom_idx, dir, q_cart, psi, k_idx, dv_psi_nl); - if (getenv("DFPT_MDBG") != nullptr && atom_idx < 2 && k_idx == 0) { - static int done[2] = {0, 0}; - if (!done[atom_idx]) { - done[atom_idx] = 1; - const int npw_dbg = psi.get_nbasis(); - const int nb_dbg = psi.get_nbands(); - for (int ib = 0; ib < nb_dbg; ++ib) { - for (int m = 0; m < nb_dbg; ++m) { - std::complex dl(0.0, 0.0); - std::complex dn(0.0, 0.0); - for (int ig = 0; ig < npw_dbg; ++ig) { - if (static_cast(dv_psi[ib].size()) == npw_dbg) { - dl += std::conj(psi(k_idx, m, ig)) * dv_psi[ib][ig]; - } - if (dv_psi_nl.size() == static_cast(nb_dbg) - && static_cast(dv_psi_nl[ib].size()) == npw_dbg) { - dn += std::conj(psi(k_idx, m, ig)) * dv_psi_nl[ib][ig]; - } - } - std::cout << "MDBG atom=" << atom_idx << " n=" << ib << " m=" << m - << " loc=(" << dl.real() << "," << dl.imag() << ")" - << " nl=(" << dn.real() << "," << dn.imag() << ")" - << std::endl; - } - } - } - } 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()) { @@ -572,7 +527,7 @@ void DFPT_Pert::dVnl_dtau(int atom_idx, int dir, // outgoing k+q basis DFPT_KQ_Basis kq; - kq.init(pw_wfc_, q_cart, k_idx); + 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) { @@ -681,11 +636,11 @@ void DFPT_Pert::d2vloc_r(int atom_idx, int da, int db, ModuleBase::Vector3 gcar; for (int ig = 0; ig < npw; ++ig) { rho_gvec(ig, gcar); - // both displacement dressings e^{i q.R} multiply on the same atom, so - // the cell sum collapses to G = 2q (mod ints); when the caller's gate - // passes (2q reciprocal) every integer G survives with its own phase - // and the kernel is exactly the plain q=0 one. When the gate fails - // there is no integer solution and the whole term vanishes (skipped). + // 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) { @@ -745,7 +700,7 @@ void DFPT_Pert::apply_d2vnl(int atom_idx, int da, int db, std::vector>> vkb_in; build_vkb(it, ia, gk_in, vkb_in); DFPT_KQ_Basis kq; - kq.init(pw_wfc_, q_eff, k_idx); + 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) { @@ -791,10 +746,12 @@ void DFPT_Pert::apply_d2vnl(int atom_idx, int da, int db, } // chi(G'') = sum_mu vkb_out,mu [ -kq_da kq_db d0 - dab // + (include_middle ? kq_da db_ + kq_db da_ : 0) ]_mu - // the |d beta> 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]; diff --git a/source/source_pw/module_dfpt/dfpt_pert.h b/source/source_pw/module_dfpt/dfpt_pert.h index c6af3f31cb9..86124f53a64 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.h +++ b/source/source_pw/module_dfpt/dfpt_pert.h @@ -31,6 +31,7 @@ class DFPT_Pert { /// 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); diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index 1ddb86c3a4c..6d5fb6f05d6 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -330,8 +330,6 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, accum_q_ = q_idx; } const int rowb = 3 * atom_idx + dir; - const ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); - const ModuleBase::Vector3 q_cart = q_frac * ucell_->G; const int nk = psi.get_nk(); const int nbands = psi.get_nbands(); @@ -350,18 +348,6 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, } } - // rho ig -> shared FFT-cell reverse map (C1/C3 pattern) - 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; - } - for (int iat = 0; iat < nat; ++iat) { for (int idir = 0; idir < 3; ++idir) { const int cola = 3 * iat + idir; @@ -430,38 +416,26 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, } // ---- same-atom anharmonic term ---- - // both displacement dressings e^{i q.R} multiply on the SAME atom, - // so the second-order potential carries wavevector 2q; the same-k - // expectation value is nonzero only when 2q folds onto a - // reciprocal vector, in which case the operator is - // lattice-periodic and lives on the integer G set (out basis = - // k + q_eff ball with q_eff = fold(2q) = 0). The |d beta> q2_frac = 2.0 * q_frac; - const ModuleBase::Vector3 q2_round(std::round(q2_frac.x), - std::round(q2_frac.y), - std::round(q2_frac.z)); - const bool q2_is_recip = ((q2_frac - q2_round).norm() < 1.0e-8); - const ModuleBase::Vector3 q_eff_cart - = (q2_frac - q2_round) * ucell_->G; - const bool q_is_recip - = ((q_frac - - ModuleBase::Vector3(std::round(q_frac.x), - std::round(q_frac.y), - std::round(q_frac.z))) - .norm() - < 1.0e-8); + // 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); const char* d2mid_env = getenv("DFPT_D2MID"); const bool include_middle = !(d2mid_env != nullptr && d2mid_env[0] == '0'); if (dbg2 && iat == atom_idx && cola == rowb) { std::cout << "DYNCHK d2gate rowb=" << rowb - << " q2recip=" << (q2_is_recip ? 1 : 0) - << " qrecip=" << (q_is_recip ? 1 : 0) << " mid=" << (include_middle ? 1 : 0) << std::endl; } - if (iat == atom_idx && cola >= rowb && q2_is_recip) { + 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) { @@ -478,7 +452,7 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, 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(), q_eff_cart, ik); + 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)) { @@ -489,14 +463,7 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, && 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 isz = kq.get_ig2isz(igl); - const int iz = isz % pert_->get_pw_wfc()->nz; - const int is = isz / pert_->get_pw_wfc()->nz; - const int ixy = pert_->get_pw_wfc()->is2fftixy[is]; - const int ix = ixy / pert_->get_pw_wfc()->fftny; - const int iy = ixy % pert_->get_pw_wfc()->fftny; - const int ig_rho = - ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + const int ig_rho = kq.get_ig_rho(igl); if (ig_rho >= 0) { x_recip[ig_rho] = chi[ib][igl]; } diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 3062392088e..db382f3d27b 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -257,7 +257,7 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { ikq_of_k_[ik] = ikq; DFPT_KQ_Basis kq; - kq.init(pw_wfc_, q_cart, ik); + 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* diff --git a/source/source_pw/module_dfpt/dfpt_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index edc4830f8c9..a83a56e770d 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -64,37 +64,18 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, const ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); const ModuleBase::Vector3 q_cart = q_frac * recip_matrix_; - // rho-ig -> FFT-cell reverse map through the shared (ix,iy,iz) triple - // (the rho/wfc stick encodings are not interchangeable, C1 finding) - 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::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_, q_cart, ik); + kq.init(pw_wfc_, pw_rho_, q_cart, ik); const int npw_kq = kq.get_npwk(); - // k+q stick index -> rho-grid ig through the shared FFT cell + // 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) { - const int isz = kq.get_ig2isz(igl); - 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; - kq2rho[igl] = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + kq2rho[igl] = kq.get_ig_rho(igl); } for (int ib = 0; ib < nbands; ++ib) { const double w = wg(ik, ib); @@ -116,8 +97,11 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, } pw_rho_->recip2real(d_recip.data(), d_r.data()); // same normalization as the GS density accumulation - // (elecstate_pw.cpp rhoBandK: w1 = wg / omega) - const double w1 = w / pw_rho_->omega; + // (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]; } @@ -125,10 +109,10 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, } // Hermitian completion at q = 0: the band loop above stores only the - // u_n^* du_n piece; the physical (real) response density also contains - // the du_n u_n^* piece, whose coefficients are conj(a_{-G}). At q = 0 - // both harmonics coincide: the response is Delta rho = 2 Re a(r), so - // symmetrize the real-space amplitude before the FFT. The resulting + // 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 @@ -138,7 +122,7 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, && 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(2.0 * a_r[ir].real(), 0.0); + a_r[ir] = std::complex(a_r[ir].real(), 0.0); } } @@ -157,10 +141,23 @@ void DFPT_Rho::compute_drho(const psi::Psi>& psi, std::abs(mfrac.y - mr[1]) < 1.0e-6 && std::abs(mfrac.z - mr[2]) < 1.0e-6) { - const int ix = (static_cast(mr[0]) % pw_rho_->nx + pw_rho_->nx) % pw_rho_->nx; - const int iy = (static_cast(mr[1]) % pw_rho_->ny + pw_rho_->ny) % pw_rho_->ny; - const int iz = (static_cast(mr[2]) % pw_rho_->nz + pw_rho_->nz) % pw_rho_->nz; - const int ig0 = ig_of_cell[(ix * pw_rho_->ny + iy) * pw_rho_->nz + iz]; + // 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); } 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 index 06a8043bfc1..39f30cba183 100644 --- a/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp @@ -6,6 +6,7 @@ #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" @@ -16,16 +17,18 @@ /** * - Tested Functions: * - DFPT_KQ_Basis::init() - enumeration of the local k+q plane-wave - * basis from an initialized ground-state k-basis by re-filtering the - * shared G grid at the shifted center k+q. - * - Accessors get_npwk / get_ig / get_ig2isz / get_gcar / - * get_gpluskq / get_gk2 / get_kplusq. + * 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. * - * The ground-state k-basis is hand-built with public members only (no FFT - * setup needed): a complex (gamma_only=false) basis on a cubic lattice with - * an ecutwfc ball large enough to contain several G shells. Every selection - * is cross-checked against an independent brute-force count over the full - * FFT grid, and the k+q -> k+q translation invariance is verified. + * 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 { @@ -57,8 +60,9 @@ class DFPTKQBasisTest : public testing::Test { protected: ModulePW::PW_Basis_K pw_; + ModulePW::PW_Basis prho_; const double lat0_ = 1.8897261254578281; - const double ecutwfc_ = 130.0; // gamma ball reaches the first G shell + const double ecutwfc_ = 520.0; // ball radius ~ 2 G shells double tpiba2_ = 0.0; double gk_ecut_ = 0.0; double ggecut_ = 0.0; @@ -77,11 +81,69 @@ class DFPTKQBasisTest : public testing::Test 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; } - // shared field setup for a cubic complex basis; builds the FFT-grid G - // set with |G|^2 <= ggecut and fills ig2isz / is2fftixy in the same - // (stick, z) layout the real distribution code produces. + // 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_); @@ -107,6 +169,7 @@ class DFPTKQBasisTest : public testing::Test 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_; @@ -118,51 +181,17 @@ class DFPTKQBasisTest : public testing::Test { pw_.kvec_c[i] = kvec_c[i]; } + FillSticks(pw_, ggecut_); - // stick layout: one stick per (ix, iy) pair that has at least one - // qualifying z plane; is2fftixy[is] = iy + ix * fftny. - std::vector is2fftixy; - std::vector ig2isz; - 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_); - std::vector stick; - for (int iz0 = 0; iz0 < nz_; ++iz0) - { - const int wiz = WrapIndex(iz0, 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 * 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]; - } + 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 @@ -205,31 +234,18 @@ class DFPTKQBasisTest : public testing::Test } }; -TEST_F(DFPTKQBasisTest, GammaQ0ReproducesBaseOrdering) +TEST_F(DFPTKQBasisTest, GammaQ0ReproducesWfcGrid) { - // single Gamma k: kmaxmod = 0, so the shared grid ball equals the Gamma - // ball and the q=0 selection must reproduce the base basis verbatim. + // 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)}); - ASSERT_EQ(pw_.npw, 7); ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + 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); - for (int igl = 0; igl < kq.get_npwk(); ++igl) - { - EXPECT_EQ(kq.get_ig(igl), igl); // ordering preserved - EXPECT_EQ(kq.get_ig2isz(igl), pw_.ig2isz[igl]); - const ModuleBase::Vector3 gcar = kq.get_gcar(igl); - EXPECT_DOUBLE_EQ(kq.get_gk2(igl), gcar * gcar); - const ModuleBase::Vector3 gp = kq.get_gpluskq(igl); - EXPECT_DOUBLE_EQ(gp.x, gcar.x); - EXPECT_DOUBLE_EQ(gp.y, gcar.y); - EXPECT_DOUBLE_EQ(gp.z, gcar.z); - } - - // every selected vector lies inside the cutoff + // 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); @@ -241,24 +257,39 @@ TEST_F(DFPTKQBasisTest, GammaQ0ReproducesBaseOrdering) 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.5): only G=(0,0,0) and G=(0,0,-1) survive the |G+k|^2 cut + // 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 * ModuleBase::TWO_PI / lat0_)}); + ModuleBase::Vector3(0.0, 0.0, 0.5 * b)}); ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_, ModuleBase::Vector3(0.0, 0.0, 0.0), 1); + 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 * ModuleBase::TWO_PI / lat0_, 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); - ASSERT_EQ(ref.size(), 2u); // G=(0,0,0) and G=(0,0,-1) const std::vector> sel = KqSet(kq); EXPECT_EQ(sel.size(), ref.size()); for (size_t i = 0; i < ref.size(); ++i) @@ -267,18 +298,57 @@ TEST_F(DFPTKQBasisTest, ShiftedCenterSelectsAsymmetricSphere) EXPECT_DOUBLE_EQ(sel[i].y, ref[i].y); EXPECT_DOUBLE_EQ(sel[i].z, ref[i].z); } +} - // indices must be unique (each selected vector corresponds to exactly - // one underlying G vector) - std::vector igs; +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) { - igs.push_back(kq.get_ig(igl)); + EXPECT_LE(kq.get_gk2(igl), gk_ecut_ + 1e-12); + EXPECT_GE(kq.get_ig_rho(igl), 0); } - std::sort(igs.begin(), igs.end()); - for (size_t i = 1; i < igs.size(); ++i) + + // 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) { - EXPECT_NE(igs[i], igs[i - 1]); + 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); } } @@ -290,13 +360,12 @@ TEST_F(DFPTKQBasisTest, TranslationInvarianceOfKQ) BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0), k1}); ModuleDFPT::DFPT_KQ_Basis a, b; - a.init(&pw_, k1, 0); - b.init(&pw_, ModuleBase::Vector3(0.0, 0.0, 0.0), 1); + 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()); - EXPECT_EQ(a.get_npwk(), b.get_npwk()); for (int igl = 0; igl < a.get_npwk(); ++igl) { - EXPECT_EQ(a.get_ig(igl), b.get_ig(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); @@ -305,59 +374,39 @@ TEST_F(DFPTKQBasisTest, TranslationInvarianceOfKQ) // shifting back by -k1 recovers the Gamma basis ModuleDFPT::DFPT_KQ_Basis c; - c.init(&pw_, ModuleBase::Vector3(0.0, 0.0, 0.0) - k1, 1); - EXPECT_EQ(c.get_npwk(), 7); + 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, NonzeroQMatchesBruteForce) -{ - const ModuleBase::Vector3 k1(0.0, 0.0, 0.5 * ModuleBase::TWO_PI / lat0_); - const ModuleBase::Vector3 q(0.5 * ModuleBase::TWO_PI / lat0_, 0.0, 0.25 * ModuleBase::TWO_PI / lat0_); - BuildBase({ModuleBase::Vector3(0.0, 0.0, 0.0), k1}); - - const ModuleBase::Vector3 centers[2] = {q, k1 + q}; - for (int ik = 0; ik < 2; ++ik) - { - ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_, q, ik); - const std::vector> ref = ReferenceSelection(centers[ik]); - 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); - } - // cutoff consistency: every retained plane wave is below the cut - for (int igl = 0; igl < kq.get_npwk(); ++igl) - { - EXPECT_LE(kq.get_gk2(igl), gk_ecut_ + 1e-12); - } - } -} - -TEST_F(DFPTKQBasisTest, InvalidOrGammaOnlyBaseIsRejected) +TEST_F(DFPTKQBasisTest, InvalidOrMismatchedBaseIsRejected) { - // null provider: valid-but-empty basis + // null providers: valid-but-empty basis ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(nullptr, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + kq.init(nullptr, nullptr, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); EXPECT_FALSE(kq.is_valid()); EXPECT_EQ(kq.get_npwk(), 0); - EXPECT_TRUE(kq.get_igl2ig().empty()); // 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_, ModuleBase::Vector3(0.0, 0.0, 0.0), 0), + 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 \ No newline at end of file +} // namespace 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 index d18bdc23e08..b7eb1eb04ca 100644 --- 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 @@ -357,7 +357,7 @@ TEST_F(DFPTPertSerialTest, ApplyDvConvolutionMatchesAnalyticMatrixElement) // expected: dpsi(G'') = sum_G' psi(G') c(G''-G'), c = analytic dVloc ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + 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); @@ -508,7 +508,7 @@ TEST_F(DFPTPertSerialTest, DVnlDtauMatchesOperatorFiniteDifference) MakeNCAtom(); const int npwk = pw_wfc_.npwk[0]; ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + 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) 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 index 8820df7eeab..fc9f4bddb89 100644 --- 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 @@ -464,7 +464,7 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) const int npwk_kq = [&]() { ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); return kq.get_npwk(); }(); std::vector> dpsi_inj(npwk_kq, std::complex(0.0, 0.0)); @@ -484,7 +484,7 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) // G'=0 plane wave and the Coulomb potential has no nonlocal part); the // GS structure-factor phase convention is exp(-i 2pi g.tau). ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); for (int adir = 0; adir < 3; ++adir) { std::complex expect_cross(0.0, 0.0); @@ -563,7 +563,7 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2GateOffGenericQ) wg(0, 1) = 0.0; ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + 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); @@ -668,7 +668,7 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) // injected dpsi on the k+q = 0 ball (arbitrary coefficients) ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); const int npwk_kq = kq.get_npwk(); std::vector> dpsi_inj(npwk_kq, std::complex(0.0, 0.0)); dpsi_inj[0] = std::complex(0.3, 0.1); 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 index 2bfd45cc156..de87d9bca22 100644 --- 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 @@ -109,7 +109,7 @@ class DFPTRhoSerialTest : public testing::Test { psi::Psi> p(1, nbands_, pw_wfc_.npwk_max, pw_wfc_.npwk[0], true); ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); dpsi.assign(1, std::vector>>(nbands_)); for (int ib = 0; ib < nbands_; ++ib) { @@ -151,7 +151,7 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoMatchesBruteForceGSpace) clist.push_back(psi(0, 0, igl)); } ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); const std::vector> dvec = data_.get_dpsi(0, 0, 0); double err2 = 0.0; @@ -218,7 +218,7 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoRealSpaceMatchesDirectSum) clist.push_back(psi(0, 0, igl)); } ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, q_cart_, 0); + kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); std::vector> dgl; std::vector> dcl; for (int jgl = 0; jgl < kq.get_npwk(); ++jgl) From d4a6e9a9c56c282bc0f4ac767a92b176777be559 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 24 Aug 2026 19:32:25 +0800 Subject: [PATCH 34/50] DFPT PLAN: P0-3 non-Gamma-q defect root-caused and fixed (drho spin factor 2, a915352cd) --- .../module_dfpt/PLAN_dfpt_implementation.md | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 9901a482c59..ddf8e62f598 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -443,7 +443,34 @@ ComputeEpsScfSyntheticStash 替换 PT 用例,串行 6/6; 端到端 sym 4×4×4 ε∞=23.35·δ(原 IPA 12.67),与 nosym ALEG 23.68、QE 23.67 同源 - - **[新缺陷登记] 非 Γ q 全链路错误(P0-3 阻塞项)**: + - **[缺陷已修复] 非 Γ q 全链路错误 → 根因=drho 缺自旋因子 2 + (a915352cd)**: + - 根因:compute_drho 用 w/Ω 且仅在 Γ 的 Hermitian 补全里补 + 2Re;QE incdrhoscf 在一切 q 用 wgt=2·w/Ω(自旋简并因子, + 非 Hermitian 补全)。q≠Γ 屏蔽强度减半 → L 点频率塌到 + −948/−148×2/183/199×2。修复后 w1=2w/Ω 恒定、Γ 补全只取 Re + (Γ 代数等价不变) + - 修复后验证(Si NC pz-vbc,4×4×4,24³ 网格):q=L(−0.5,0.5,0.5) + 2π/a → 100.49×2/380.41/402.11/485.93×2 vs QE + 101.61×2/380.54/402.24/486.28×2(0.1–1.1%);Γ 保持 + 517.491(QE 517.633);bare(NOSC vs QE niter_ph=1) + −2281.83/−578.00×2/−528.59/−278.82×2 vs −2282.01/ + −577.16×2/−527.51/−277.69×2(0.008–0.4%)→ 裸链 + (dV/dψ/term2/term3/Ewald)整体正确 + - 排除过程存档:dVnl 链逐位复现(becp/dcbecp/term_a/term_b + vs dvqpsi_us_only.f90,relmax≤2.3e-15);vkb/Simpson/radial_vq + 复现(≤4.4e-16);CG 残差直接证明 P(H−εS)P·dψ=P_c·rhs + (~6e-9);QE D(L) 从 matdyn 本征集重建(U 全实 → D 实对称, + 逐元素 diff 定位 ele 全错、ion 正确);NOSC 对照 QE bare + 把嫌疑压缩到屏蔽链 → drho 归一化 + - 遗留:0.1–1.1% 残差呈均匀绝对 D 误差(最小 ω 相对误差最大) + —待查(k 权重/FFT 细节);QE fildrho 记录与我们的 drho 逐 + 元素对照通道未打通(模式→笛卡尔重建后 G 键匹配失败, + 疑记录顺序/约定,未继续);DFPT_RHSDUMP/NLDUMP/MDBG 探针 + 已从 dfpt_pert.cpp 清除(DFPT_DPDUMP/DRHODIR 亦然), + dfpt_pw.cpp 的预存探针(NOSC/JPROBE/BPT/XCS/NOXC/YCHK/ + PTCROSS/DKCHK/ALEG/MIX_BETA/MDBG)未动 + - **[旧缺陷登记归档] 非 Γ q 全链路错误(修复前记录,见上)**: QE 7.2 本地参照(同 UPF/胞/ecut/4×4×4 网格): q=L(0.5,0,0) → 125.39×2/239.10/473.79×2/496.34 cm⁻¹; q=Γ-L 1/4 → 102.75×2/144.31/494.62×2/502.63。我们: @@ -462,10 +489,6 @@ dn≠0 标签折叠路径);build_occ_kq 的 dn≠0 G 向量匹配 (2-k case dn=(1,0,0) 当时验证过,64k 大量 dn≠0 未验); term2 cross 的逐元素正确性(DFPT_XB 打印未对照) - - 下一步(调试战役):① XB 逐元素 + 手算矩阵元对照 - (2-k case 最小复现);② 独立 python FD 实现 dV_loc(q) - 系数在真实 gcar 网格上对照;③ 超胞 FD 声子端到端参照 - (4×1×1 胞 Γ 点,q=1/4 对应) - 8×8×8 nosym ALEG 验收运行中(~4h,q=0 不受本缺陷影响) - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` - dfpt_qfile + QList::read_from_file 端到端首次运行:q=(0.5,0,0),k={Γ,L}, From 63060a78fc74db46d5cdd292706bc8bb5ccda324 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 24 Aug 2026 21:15:18 +0800 Subject: [PATCH 35/50] DFPT B2: formalize the phonon output (multi-q report, LO-TO corrected frequencies, data-layer loto direction) - DFPT_PW_Data: loto_dir_ (unit-normalized setter, isotropic (1,1,1)/sqrt(3) default) and phon_freq_loto_ storage. - DFPT_Phon: diagonalize_loto re-diagonalizes the Gamma matrix after add_loto and stores signed frequencies separately (plain phon_freq(0) stays intact); format_q_report/format_loto_report provide deterministic fixed-precision blocks (header with direct q coordinates and the correction direction). - DFPT_PW::run uses data_.get_loto_dir() instead of the hardcoded (1,1,1)/sqrt(3); new accessors get_nq/get_qvec/get_loto_dir/ get_phon_freq_loto/set_loto_dir plus the format forwarders. - esolver run_post_process prints one block per q of the list plus the LO-TO Gamma block when enabled; tensor blocks only print when computed. - Serial regression: 3 new cases (direction normalization, closed-form LO-TO spectrum {0, 13/12*pref}, char-exact format strings); phon 12/12, ctest 12/12, all 4 DFPT serial tests pass. - End-to-end smoke (Gamma, compute_q0+loto, 4x4x4): TO 517.490709 unchanged, LO-TO block along (0.577350 0.577350 0.577350), eps_inf 23.6825 and Z*=-1.19928d for both atoms vs QE 23.6685/-1.19765 (0.13%). QE itself prints same-sign Z* with asr Sum=-2.395 for this setup; the acoustic-branch lift is the faithful consequence, not a defect. - No docs change: module_dfpt is design-phase, no INPUT parameter touched. --- source/source_esolver/esolver_dfpt_pw.cpp | 36 ++++-- .../module_dfpt/PLAN_dfpt_implementation.md | 36 ++++-- source/source_pw/module_dfpt/dfpt_phon.cpp | 75 ++++++++++++ source/source_pw/module_dfpt/dfpt_phon.h | 19 +++ source/source_pw/module_dfpt/dfpt_pw.cpp | 38 +++++- source/source_pw/module_dfpt/dfpt_pw.h | 26 ++++ source/source_pw/module_dfpt/dfpt_pw_data.cpp | 10 ++ source/source_pw/module_dfpt/dfpt_pw_data.h | 16 +++ .../test_serial/dfpt_phon_serial_test.cpp | 112 ++++++++++++++++++ 9 files changed, 344 insertions(+), 24 deletions(-) diff --git a/source/source_esolver/esolver_dfpt_pw.cpp b/source/source_esolver/esolver_dfpt_pw.cpp index 0c5690b671c..96037a25c17 100644 --- a/source/source_esolver/esolver_dfpt_pw.cpp +++ b/source/source_esolver/esolver_dfpt_pw.cpp @@ -365,28 +365,40 @@ void ESolver_DFPT_PW::run_post_process(UnitCell& ucell) { return; } - // design-phase validation output (single-rank runs); the io layer - // integration lands with the data-layer consolidation stage - const std::vector freqs = dfpt_->get_phonon_freq(0); - std::cout << " DFPT phonon frequencies at q #0 (cm^-1):" << std::endl; - for (size_t im = 0; im < freqs.size(); ++im) + // 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 << " mode " << im << " : " << freqs[im] << " cm^-1" << std::endl; + 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(); - std::cout << " DFPT dielectric tensor (epsilon_inf):" << std::endl; - for (int a = 0; a < eps.nr; ++a) + if (eps.nr == 3 && eps.nc == 3) { - std::cout << " "; - for (int b = 0; b < eps.nc; ++b) + std::cout << " DFPT dielectric tensor (epsilon_inf):" << std::endl; + for (int a = 0; a < eps.nr; ++a) { - std::cout << eps(a, b) << " "; + std::cout << " "; + for (int b = 0; b < eps.nc; ++b) + { + std::cout << eps(a, b) << " "; + } + std::cout << std::endl; } - 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) { diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index ddf8e62f598..34f39673268 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -433,8 +433,9 @@ aleg_crosscheck+PTCROSS,dfpt_pw.cpp)、DFPT_XCS/XCDBG/ DKCHK/NOXC/NOSC/YCHK/BPT/MDBG/JPROBE;dpsi_efield stash (dfpt_pw_data)随 ALEG 保留 - - [ ] P0-3 B0 收尾:8×8×8 过夜(ε∞/Z*/D 密网格收敛);非 Γ q 物理级验证 - (密 k 色散 vs 超胞 FD);sym1 星旋转各向异性处理或记录在案 + - [ ] P0-3 B0 收尾:8×8×8 过夜(ε∞/Z*/D 密网格收敛);sym1 星旋转各向异性处理或记录在案 + (非 Γ q 物理级验证已由 QE 直接锚定完成:L 点 0.1–1.1%、bare 链 + 0.008–0.4%,见下方缺陷修复条目;超胞 FD 参照降级为可选项) - **compute_eps SCF 化(完成,98c7f114c)**:solve_efield_resp 转正(QE solve_e 顺序:Y 腿后、位移 solve 前); compute_eps 改消耗 pos_resp+dpsi_efield 收缩 @@ -500,11 +501,32 @@ −0.01996/−0.00808/+0.06822)——稀疏 2-k 采样下 q=±L 采样不同跃迁集, 接近一致即内部自洽;4 个负本征值(虚频)为 2-k 超稀采样的性质,非 q 路径 bug(±q 忠实重现);物理级验证需密网格/超胞 FD - - [ ] B2 输出正式化 - - run_post_process design-phase std::cout → 正式输出:多 q 布局、LO-TO - 修正后频率(每方向);loto 方向经数据层传递,消除 run() 中 - (1,1,1)/√3 硬编码;输出格式回归测试 - - [ ] B3 Kerker 预条件混合 + - [x] B2 输出正式化 `(本轮)` + - 多 q 频率报告:DFPT_Phon::format_q_report(每 q 一块,表头带 + direct q 坐标,模式行定点 6 位小数);esolver run_post_process + 循环 get_nq() 输出,tensor 块仅在已计算时打印(compute_q0=false + 不再输出空表头) + - LO-TO 修正频率:DFPT_Phon::diagonalize_loto(add_loto 后对 + 修正 dynmat(0) 再对角化,存 phon_freq_loto,原 phon_freq(0) + 不动);format_loto_report 沿数据层方向输出(无修正频率时 + 返回空串) + - loto 方向经数据层:DFPT_PW_Data::loto_dir_(默认 (1,1,1)/√3, + setter 归一化、零向量保持原值);DFPT_PW::set_loto_dir/ + get_loto_dir 公开 API;run() 中硬编码删除,改用 + data_.get_loto_dir()(一般方向控制随 A 阶段 irrep 机制) + - 格式回归测试:phon 串行 3 用例(LotoDirNormalization、 + DiagonalizeLotoClosedForm——xx 2×2 块 {0,13/12·pref} 闭式、 + FormatReportsRegression——逐字符字符串钉死),12/12 + - 端到端冒烟(Γ, compute_q0+loto, 4×4×4):TO 517.490709×3 + 不变;LO-TO 块沿 (0.577350 0.577350 0.577350) 输出,声学支 + −7.325→+73.208 cm⁻¹、光学支不动;ε∞=23.6825、 + Z*₁=Z*₂=−1.19928δ。**观察登记(非缺陷)**:QE 同 setup 自身 + 打印 Z*₁=Z*₂=−1.19765(asr 前 Sum=−2.395,asr 后全零), + 我们逐值一致(0.13%,与 D 同残差量级);同号 Z* 经键心反演 + 对称性成立(F₁(E)=−F₂(−E) → Z₁=Z₂),故 LO-TO 抬升的是 + 声学支组合——与 QE 输入自洽。ΣZ* 求和规则的表述与 asr 语义 + 留待后续物理阶段讨论 + - [ ] B3 Kerker 预条件混合 - DFPT_Rho 内自实现 |G+q|²/(|G+q|²+a²) 预条件(不引 charge_mixing.h); mix_type 支持 plain/kerker;验收:λ_A1≈−2.2 模型问题 β=0.7 收敛 (JPROBE 复用)、金刚石频率与 β 无关、默认 β 回调并文档记录 diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index 6d5fb6f05d6..c8fb24b1c1b 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -20,7 +20,9 @@ #include #include #include +#include #include +#include #include namespace ModuleDFPT { @@ -29,6 +31,25 @@ DFPT_Phon::DFPT_Phon() {} DFPT_Phon::~DFPT_Phon() {} +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; @@ -698,6 +719,60 @@ void DFPT_Phon::add_loto(const ModuleBase::Vector3& qhat, DFPT_PW_Data& data.set_dynmat(0, dyn); } +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)); +} + +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(); +} + +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(); +} + 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 diff --git a/source/source_pw/module_dfpt/dfpt_phon.h b/source/source_pw/module_dfpt/dfpt_phon.h index 9a70619b99a..25c99a355e2 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.h +++ b/source/source_pw/module_dfpt/dfpt_phon.h @@ -13,6 +13,8 @@ #include "source_cell/unitcell.h" #include "source_psi/psi.h" +#include + namespace ModulePW { class PW_Basis; } @@ -58,6 +60,23 @@ class DFPT_Phon { 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; /// 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) / diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index db382f3d27b..a6e4b87f2d7 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -1625,19 +1625,43 @@ void DFPT_PW::run() { 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 a documented default - // direction (isotropic for cubic crystals; a general q->0 - // direction control arrives with the irrep machinery of stage A) - const double inv = 1.0 / std::sqrt(3.0); - pimpl_->phon_.add_loto(ModuleBase::Vector3(inv, inv, inv), pimpl_->data_); + // 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(); } @@ -1680,4 +1704,8 @@ 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 33bdc3aa614..c1258133712 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.h +++ b/source/source_pw/module_dfpt/dfpt_pw.h @@ -10,6 +10,7 @@ #define DFPT_PW_H #include "source_base/matrix.h" +#include "source_base/vector3.h" #include "source_cell/unitcell.h" #include "source_psi/psi.h" @@ -87,6 +88,31 @@ class DFPT_PW { 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_; diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index 4e965f317c5..c2f86a41c15 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -9,6 +9,8 @@ #include "dfpt_pw_data.h" #include "source_lcao/module_dftu/dftu.h" +#include + namespace ModuleDFPT { DFPT_PW_Data::DFPT_PW_Data() {} @@ -329,6 +331,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; } diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index e89b0dced6c..21b57bca248 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -96,6 +96,18 @@ class DFPT_PW_Data { 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 @@ -204,6 +216,10 @@ class DFPT_PW_Data { 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_; 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 index fc9f4bddb89..3efc606373c 100644 --- 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 @@ -869,3 +869,115 @@ TEST_F(DFPTPhonSerialTest, CheckSumRuleAtGamma) 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 + ModuleBase::ComplexMatrix dyn0(6, 6, true); + data_.set_dynmat(0, dyn0); + 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); + // temporarily make the cell two-atom for the mass lookup + 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].mass = 12.0; + ucell_.atoms[1].mass = 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; + + 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); +} From 6a3caf54bf03ff70b2ae8d0c98a6a9c9e1cb21fa Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 24 Aug 2026 22:18:14 +0800 Subject: [PATCH 36/50] DFPT B3: Kerker-preconditioned density mixing in DFPT_Rho - DFPT_Rho::init gains mix_type (plain/kerker) and kerker_a2 (1/lat0^2); no charge_mixing.h dependency, screen f_g = |G+q|^2/(|G+q|^2+a^2) built with the v_hartree_q convention (gcar + q_frac*G). Screen both inputs, plain_mix, add the screened part back: mixed = rin + beta*f*(out-rin) (QE semantics, stored density stays physical; |G+q|=0 harmonic frozen, consistent with its drop in compute_drho). Init signature extended with an explicit kerker_a2 argument (no default arg; both call sites updated). - Wiring: env DFPT_MIX_TYPE / DFPT_KERKER_A2 design-phase knobs mirroring the DFPT_MIX_BETA precedent; default plain keeps behavior identical and the beta=0.4 default (and its stability rationale) stays documented in the init comment. No INPUT parameter change: no docs update required (env knobs are internal calibration aids, same category as DFPT_MIX_BETA). - Tests (dfpt_rho_serial, 6 -> 8): analytic first Kerker step; lambda=-2.2 stiff-shell model problem where plain beta=0.7 diverges (residual > 1) and kerker converges (< 1e-8) to the target. - Fixed latent breaks masked by a stale test binary since a915352cd: kq0.init not updated to the 4-arg DFPT_KQ_Basis::init signature, and the brute-force references missing the band-weight spin factor 2. - End-to-end (L point, 4x4x4, abacus_pw_para v3.11.0-beta8): plain beta=0.7 diverges (|drho| -> 1e20); kerker beta=0.7 converges in 1393 s (vs 2332 s plain beta=0.4); frequencies identical across plain 0.4 / kerker 0.4 / kerker 0.7 to 8-9 digits (100.487828 x2 / 380.41385 / 402.10912 / 485.93199 x2 cm^-1). - Verification: OMP_NUM_THREADS=1 ctest -R 'MODULE_CELL_klist_test$| MODULE_CELL_reciprocal_grid_test|MODULE_CELL_qlist_test| MODULE_CELL_little_group_test|MODULE_DFPT' -> 12/12; serial suites pert 8 / phon 12 / q0 6 / rho 8 all pass; governance --staged clean except the expected no-docs-needed WARNING recorded here. --- .../module_dfpt/PLAN_dfpt_implementation.md | 24 ++- source/source_pw/module_dfpt/dfpt_pw.cpp | 21 ++- source/source_pw/module_dfpt/dfpt_rho.cpp | 51 +++++- source/source_pw/module_dfpt/dfpt_rho.h | 23 ++- .../test_serial/dfpt_rho_serial_test.cpp | 158 +++++++++++++++++- 5 files changed, 254 insertions(+), 23 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 34f39673268..8b3c4d2b0d9 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -526,10 +526,32 @@ 对称性成立(F₁(E)=−F₂(−E) → Z₁=Z₂),故 LO-TO 抬升的是 声学支组合——与 QE 输入自洽。ΣZ* 求和规则的表述与 asr 语义 留待后续物理阶段讨论 - - [ ] B3 Kerker 预条件混合 + - [x] B3 Kerker 预条件混合(2026-08-24 完成) - DFPT_Rho 内自实现 |G+q|²/(|G+q|²+a²) 预条件(不引 charge_mixing.h); mix_type 支持 plain/kerker;验收:λ_A1≈−2.2 模型问题 β=0.7 收敛 (JPROBE 复用)、金刚石频率与 β 无关、默认 β 回调并文档记录 + - 实现:`DFPT_Rho::init` 增 `mix_type`("plain"/"kerker") 与 `kerker_a2` + (1/lat0² 单位,与 |G+q|² 同纲);筛选 f_g=|G+q|²/(|G+q|²+a²) 用 + v_hartree_q 同一约定(gcar+q_frac·G);"screen 双方→plain_mix→补回 + 被筛部分"得 mixed=rin+βf(out−rin)(QE 语义:存量密度保持物理量, + 非筛选缩放),|G+q|=0 谐波冻结(与 compute_drho 丢弃一致)。接线层 + env `DFPT_MIX_TYPE`/`DFPT_KERKER_A2`(设计期校准旋钮,镜像 + DFPT_MIX_BETA 先例;默认 plain → 行为逐字节不变,默认 β=0.4 回调 + 见 dfpt_pw.cpp init 注释与 dfpt_rho.h 类注释) + - 串行验收(新增 2 用例,rho serial 6→8):解析首步 + mixed=β·f·out;模型问题 out=D·in+s(最小 |G+q| 壳 D=λ_A1=−2.2, + 其余 0.3)β=0.7:plain 残差>1 发散、kerker(a²=9w2_min) <1e-8 + 收敛到 target —— 无需 JPROBE(DFPT_DEBUG 残差轨迹即可作证), + JPROBE 可进入清理队列 + - 端到端(L 点, 4×4×4):plain β=0.7 残差振荡 >1、|drho|→1e20 + 爆发散;kerker β=0.7 收敛(1393 s,138 SCF 迭代/6 位移,反快于 + plain β=0.4 的 2332 s);频率与 (mix_type, β) 无关——三配置 + 100.487828²/380.41384/402.10912/485.93199² 一致至 8–9 位 + (参考 plain β=0.4:100.488/380.414/402.109/485.932) + - 附带修复(陈旧二进制掩盖的 a915352cd 遗留):rho serial 测试 + kq0.init 未跟进 4 参签名、brute-force 参考缺自旋因子 2 —— + 四个串行二进制全部重建后 pert 8/phon 12/q0 6/rho 8 通过, + ctest 12/12 - [ ] B4 数据层收编 - 收敛台账(converged_/residuals_/current_iter_ 按 (q,irrep))并入 DFPT_PW_Data;删除 DFPT_IrrepData 适配层与 get_dpsi_obj static dummy; diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index a6e4b87f2d7..105e16ae7e3 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -188,7 +188,10 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, // 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 env knob is a design-phase calibration aid + // |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); @@ -196,7 +199,21 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, mix_beta = parsed; } } - pimpl_->rho_.init(nspin, nrxx, pw_rho, pw_wfc, ucell.G, "plain", mix_beta); + 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_; diff --git a/source/source_pw/module_dfpt/dfpt_rho.cpp b/source/source_pw/module_dfpt/dfpt_rho.cpp index a83a56e770d..8cb12ae5359 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.cpp +++ b/source/source_pw/module_dfpt/dfpt_rho.cpp @@ -32,17 +32,20 @@ DFPT_Rho::~DFPT_Rho() { 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) { + const std::string& mix_type, double mix_beta, + double kerker_a2) { nspin_ = nspin; nrxx_ = nrxx; pw_rho_ = pw_rho; pw_wfc_ = pw_wfc; recip_matrix_ = recip_matrix; mix_beta_ = mix_beta; - if (mix_type != "plain") + mix_type_ = mix_type; + kerker_a2_ = kerker_a2; + if (mix_type != "plain" && mix_type != "kerker") { ModuleBase::WARNING_QUIT("DFPT_Rho", - "only plain mixing is supported in the design phase"); + "unsupported mix_type, expected plain or kerker"); } delete mixer_; mixer_ = new Base_Mixing::Plain_Mixing(mix_beta_); @@ -250,11 +253,42 @@ void DFPT_Rho::mix_drho(int q_idx, DFPT_PW_Data& data) { } const std::vector>& rin = drho_in_[q_idx][0]; std::vector> mixed(npw); - mixer_->plain_mix(mixed.data(), - rin.data(), - out.data(), - npw, - std::function*)>()); + // 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; @@ -269,7 +303,6 @@ void DFPT_Rho::mix_drho(int q_idx, DFPT_PW_Data& data) { // 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 ModuleBase::Vector3 q_frac = data.get_qvec(q_idx); 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); diff --git a/source/source_pw/module_dfpt/dfpt_rho.h b/source/source_pw/module_dfpt/dfpt_rho.h index cde8bf0beec..f1b696d88d8 100644 --- a/source/source_pw/module_dfpt/dfpt_rho.h +++ b/source/source_pw/module_dfpt/dfpt_rho.h @@ -62,9 +62,21 @@ class XC_First_Order { * harmonic is dropped when -q falls on a reciprocal-lattice vector (charge * conservation, notably at q = Gamma). * - * mix_drho applies plain mixing on the q-shifted coefficients: - * drho_in <- drho_in + beta (drho_out - drho_in) + * 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: @@ -74,7 +86,8 @@ class DFPT_Rho { 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); + 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, @@ -113,6 +126,10 @@ class DFPT_Rho { ///< 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; Base_Mixing::Plain_Mixing* mixer_ = nullptr; 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 index de87d9bca22..ea32f789d27 100644 --- 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 @@ -101,7 +101,7 @@ class DFPTRhoSerialTest : public testing::Test q_cart_ = q_d_ * G_; data_.init(&qlist_, 1, nbands_, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); - rho_.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "plain", 0.4); + rho_.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "plain", 0.4, 0.0); } void FillRandomStates(psi::Psi>& psi, @@ -170,8 +170,9 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoMatchesBruteForceGSpace) 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 = (w / omega) * sum_G c*_G d_{G+Delta} with the GS density - // normalization (elecstate rhoBandK w1), brute-forced over the lists + // 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) { @@ -187,7 +188,7 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoMatchesBruteForceGSpace) } } } - aref *= wg(0, 0) / pw_rho_.omega; + aref *= 2.0 * wg(0, 0) / pw_rho_.omega; err2 += std::norm(drho_g[ig] - aref); ref2 += std::norm(aref); } @@ -253,8 +254,10 @@ TEST_F(DFPTRhoSerialTest, ComputeDrhoRealSpaceMatchesDirectSum) } 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)); - // same GS normalization (w / omega) as the stored manifest density - const double ref = 2.0 * (wg(0, 0) / pw_rho_.omega) * (std::conj(u) * du * eq).real(); + // 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); } @@ -278,11 +281,11 @@ TEST_F(DFPTRhoSerialTest, ChargeConservationAtGamma) 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); + 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, ModuleBase::Vector3(0.0, 0.0, 0.0), 0); + 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) @@ -429,3 +432,142 @@ TEST_F(DFPTRhoSerialTest, VHartreeQClosedFormAndZeroMode) 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); + } +} From 3904ecf65a75de87aa74194b175beeb0b8493c5a Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Mon, 24 Aug 2026 23:36:56 +0800 Subject: [PATCH 37/50] DFPT B4: sink the (q,irrep) SCF ledger into DFPT_PW_Data, retire the DFPT_IrrepData adapter - DFPT_PW_Data: the write-only single-slot ledger (set_current_iter(int)/ set_converged(bool)/add_residual(double)) is replaced by the (q,irrep)-keyed six-accessor ledger sunk from DFPT_IrrepData (std::map value members, missing keys read as not-converged / empty history / iteration 0, clean() drops the ledger). The irrep dimension stays as the stage-A slot: the fallback irrep 0 carries the full 3N displacement basis. The new / includes are required by the map value members the header owns. - DFPT_IrrepData adapter deleted (git rm): its irrep==0 forwarding of dpsi/drho/dv duplicated the existing per-q data API, and its own keyed maps moved to the data layer. get_dpsi_obj (static dummy, zero callers) removed. Both CMakeLists updated, including the pw_run_test source list. - run() outer-while accounting made honest: current_iter now increments per pass and convergence is worst-final-displacement-residual < conv_thr instead of an unconditional single pass. An unconverged pass re-runs the full solve (solve_displacement restarts from a zero input), bounded by max_iter outer passes, with the residual history keeping a record. Behavior on converged runs is bit-identical. - solve_displacement / solve_efield_resp: write-only inner ledger writes removed; per-displacement state stays local and the final residual returns to run() for aggregation. - Tests: dfpt_irrep_data_test.cpp renamed/rewritten as dfpt_pw_data_test.cpp (target MODULE_DFPT_pw_data_test, 5 cases: QList delegation, bound-safe accessors with the (q,spin) signature, setter round trip, keyed-ledger independence + clean() reset, U0 reservation). - Verification: OMP_NUM_THREADS=1 ctest -R 'MODULE_CELL_klist_test$| MODULE_CELL_reciprocal_grid_test|MODULE_CELL_qlist_test| MODULE_CELL_little_group_test|MODULE_DFPT' -> 12/12 (pw_data_test fills the retired irrep_data_test slot); serial suites pert 8 / phon 12 / q0 6 / rho 8 all pass; end-to-end L-point default-config smoke (abacus_pw_para v3.11.0-beta8) reproduces the reference frequencies bit-consistently (100.487828/100.487829/380.413847/402.109158/ 485.931988/485.931988 cm^-1, TOTAL 2332 s, same as the pre-B4 reference). Governance --staged: header-include warning justified by map value members; no INPUT behavior change so no docs update required. --- source/source_pw/module_dfpt/CMakeLists.txt | 2 - .../module_dfpt/PLAN_dfpt_implementation.md | 26 +++- .../source_pw/module_dfpt/dfpt_irrep_data.cpp | 119 --------------- .../source_pw/module_dfpt/dfpt_irrep_data.h | 103 ------------- source/source_pw/module_dfpt/dfpt_pw.cpp | 38 +++-- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 31 +++- source/source_pw/module_dfpt/dfpt_pw_data.h | 33 +++-- .../source_pw/module_dfpt/test/CMakeLists.txt | 6 +- ...ep_data_test.cpp => dfpt_pw_data_test.cpp} | 135 +++++++++--------- 9 files changed, 162 insertions(+), 331 deletions(-) delete mode 100644 source/source_pw/module_dfpt/dfpt_irrep_data.cpp delete mode 100644 source/source_pw/module_dfpt/dfpt_irrep_data.h rename source/source_pw/module_dfpt/test/{dfpt_irrep_data_test.cpp => dfpt_pw_data_test.cpp} (68%) diff --git a/source/source_pw/module_dfpt/CMakeLists.txt b/source/source_pw/module_dfpt/CMakeLists.txt index 6ff9239d2b2..55ef7853867 100644 --- a/source/source_pw/module_dfpt/CMakeLists.txt +++ b/source/source_pw/module_dfpt/CMakeLists.txt @@ -3,7 +3,6 @@ set(MODULE_NAME module_dfpt) set(SOURCES dfpt_pw.cpp dfpt_pw_data.cpp - dfpt_irrep_data.cpp dfpt_kq_basis.cpp dfpt_pert.cpp dfpt_stern.cpp @@ -17,7 +16,6 @@ set(SOURCES set(HEADERS dfpt_pw.h dfpt_pw_data.h - dfpt_irrep_data.h dfpt_kq_basis.h dfpt_pert.h dfpt_stern.h diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index 8b3c4d2b0d9..de86a17be8b 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -232,7 +232,7 @@ - 后期漂移根因(本轮确诊):残差降至 5e-5 后指数增长(1.27×/iter)、|in| 恒定而 out 偏离 → 垃圾方向与物理分量正交、混合映射本征值 μ=1.2765 恒定(纯本征模);本征模身份 = {200} 壳 6 矢等幅实系数 + {111} 壳 8 矢 ±π/4 相位的 Hermitian 实 A1 呼吸模(seed ~1e-6 舍入级);均匀探针实验(DFPT_JPROBE:注入纯 A1 模 + rhs 去 dV_ext 单迭代直测线性映射)给出 λ_A1 = −2.229(Hartree-only −3.180,XC 削减到 −2.23)——非符号 bug,是最小 G 壳的 Coulomb 刚性(4π/G² 硬核):plain mixing 收敛条件 −2/β+1<λ 要求 β<0.62,物理 T2 模 λ=−1.42(小 G 头部含量少)在 β=0.7 恰好可收敛,故固定点正确而 A1 通道发散;β=0.4 时 μ_A1=−0.29 稳定 - 修复:默认 mix_beta 0.7→0.4(注释记录测得的 |λ|~2.2 与 β 上界 2/(1+|λ_min|),留裕量至 |λ|~5);DFPT_MIX_BETA env 旋钮保留;β=0.4 时 6 位移全部经收敛旗标退出(平均 ~38 iter,总 228),频率/ele 矩阵与 β 无关逐位一致(固定点正确性再验证),收敛 drho manifest 干净(|FD| 比率 0.99994、cos 0.9993、逐点相对差 3.8%);后续正解是 Kerker 型预条件混合(随 B 阶段排期) - ε∞/Z* 打印为空(随 B 阶段);调试插桩(PTCHK/DYNCHK/MDBG/JPROBE dump/VKBCHK/drho dump/DFPT_MIX_BETA env)收尾节点统一清理评审 -- [ ] B 工程化收编(2026-08-18 修订:B1 INPUT 接线 → B0 全流程验证 → B2 输出正式化 → B3 Kerker → B4 数据层) +- [x] B 工程化收编(2026-08-18 修订:B1 INPUT 接线 → B0 全流程验证 → B2 输出正式化 → B3 Kerker → B4 数据层;2026-08-24 全部完成) - 修订依据的差距盘点(代码 vs 计划交叉核对,2026-08-18): ① `set_compute_q0`/`set_loto` 全仓库无调用者(死代码,q0/loto 分支不可达); ② `set_parameters("dfpt.in")` 空桩 + esolver 硬编码 `set_qmesh(1,1,1)`/conv_thr/max_iter(非 Γ q 无法从输入驱动); @@ -552,10 +552,32 @@ kq0.init 未跟进 4 参签名、brute-force 参考缺自旋因子 2 —— 四个串行二进制全部重建后 pert 8/phon 12/q0 6/rho 8 通过, ctest 12/12 - - [ ] B4 数据层收编 + - [x] B4 数据层收编(2026-08-24 完成) - 收敛台账(converged_/residuals_/current_iter_ 按 (q,irrep))并入 DFPT_PW_Data;删除 DFPT_IrrepData 适配层与 get_dpsi_obj static dummy; 测试迁移;保留 (q,irrep) 接口形状;run() 外层 while 记账语义梳理 + - 台账:DFPT_PW_Data 单槽(set_current_iter(int) 等,生产代码只写 + 不读)替换为 (q,irrep) 键控六访问器(map 值成员,缺键读 + false/空/0,clean() 清空);solve_displacement / + solve_efield_resp 内层只写语句删除——位移级状态回归函数局部量, + 末残差经返回值交 run() 聚合 + - 适配层:dfpt_irrep_data.{h,cpp} 删除(git rm);其 irrep==0 转发 + 的 dpsi/drho/dv 访问本就是数据层既有 API;get_dpsi_obj 无调用者 + 纯删 + - run() 记账语义:外层 while 每遍 current_iter+1(原先不递增、 + 无条件置 converged 的退化单遍改为诚实台账)——一遍 = 3N 位移各自 + 完整收敛 + 2n+1 累积,遍残差取各位移末残差最坏值,worst< + conv_thr 才置收敛;未收敛遍会重启全量求解(solve_displacement + 从零输入起),max_iter_ 界内诚实重试,残差史留痕。收敛工况下 + 行为与旧版逐位一致 + - 测试迁移:dfpt_irrep_data_test.cpp → dfpt_pw_data_test.cpp + (目标 MODULE_DFPT_pw_data_test,5 用例:QList 委托、边界安全 + ((q,spin) 二参签名)、roundtrip、键控台账独立性+clean() 复位、 + U0 预留);两处 CMakeLists 同步(含 pw_run_test 源列表去 + irrep_data) + - 验证:ctest 12/12(pw_data_test 顶替 irrep_data_test 槽位); + 4 串行套件 pert 8/phon 12/q0 6/rho 8;端到端 L 点默认配置 + 冒烟逐位复现参考频率(见提交) - [ ] 插桩清理评审(B0/B3 后统一):PTCHK/DYNCHK(+2/4/XB)/MDBG/JPROBE/ VKBCHK/VKBEL/OCCCHK/XB/ZDBG/BPT/NOSC/D2MID/DFPT_MIX_BETA/drho dump (JPROBE 留 B3 验收后删) diff --git a/source/source_pw/module_dfpt/dfpt_irrep_data.cpp b/source/source_pw/module_dfpt/dfpt_irrep_data.cpp deleted file mode 100644 index 94bbfe388f3..00000000000 --- a/source/source_pw/module_dfpt/dfpt_irrep_data.cpp +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @file dfpt_irrep_data.cpp - * @brief Implementation of the irrep-indexed DFPT data adapter (Phase 4). - * @author Mohan Chen (added on 2026-05-18) - * @note Phase 4 (DFPT wiring) interim layer: simulates the option-2. - * signatures on top of the current per-q storage. The irrep index is - * forwarded to the underlying per-q accessors (an empty, fully - * symmetric placeholder irrep), matching the one-irrep-per-q behavior - * currently produced by ModuleCell::QList::get_irreps(). - */ -#include "dfpt_irrep_data.h" - -namespace ModuleDFPT { - -DFPT_IrrepData::DFPT_IrrepData(DFPT_PW_Data& data) : data_(data) {} - -int DFPT_IrrepData::get_nq() const -{ - return data_.get_nq(); -} - -int DFPT_IrrepData::get_nirr(int q_idx) const -{ - return data_.get_nirr(q_idx); -} - -std::vector DFPT_IrrepData::get_irrep_modes(int q_idx, int irrep) const -{ - return data_.get_irrep_modes(q_idx, irrep); -} - -void DFPT_IrrepData::set_dpsi(int q_idx, int irrep, int k_idx, int band_idx, - const std::vector>& psi) -{ - (void)irrep; - data_.set_dpsi(q_idx, k_idx, band_idx, psi); -} - -std::vector> DFPT_IrrepData::get_dpsi(int q_idx, int irrep, int k_idx, - int band_idx) const -{ - (void)irrep; - return data_.get_dpsi(q_idx, k_idx, band_idx); -} - -void DFPT_IrrepData::set_drho_r(int q_idx, int irrep, int spin, const std::vector& rho) -{ - (void)irrep; - data_.set_drho_r(q_idx, spin, rho); -} - -std::vector DFPT_IrrepData::get_drho_r(int q_idx, int irrep, int spin) const -{ - (void)irrep; - return data_.get_drho_r(q_idx, spin); -} - -void DFPT_IrrepData::set_drho_g(int q_idx, int irrep, int spin, - const std::vector>& rho) -{ - (void)irrep; - data_.set_drho_g(q_idx, spin, rho); -} - -std::vector> DFPT_IrrepData::get_drho_g(int q_idx, int irrep, int spin) const -{ - (void)irrep; - return data_.get_drho_g(q_idx, spin); -} - -void DFPT_IrrepData::set_dv_r(int q_idx, int irrep, int spin, const std::vector& v) -{ - (void)irrep; - data_.set_dv_r(q_idx, spin, v); -} - -std::vector DFPT_IrrepData::get_dv_r(int q_idx, int irrep, int spin) const -{ - (void)irrep; - return data_.get_dv_r(q_idx, spin); -} - -void DFPT_IrrepData::set_converged(int q_idx, int irrep, bool flag) -{ - converged_[std::make_pair(q_idx, irrep)] = flag; -} - -bool DFPT_IrrepData::get_converged(int q_idx, int irrep) const -{ - std::map, bool>::const_iterator it - = converged_.find(std::make_pair(q_idx, irrep)); - return it != converged_.end() ? it->second : false; -} - -void DFPT_IrrepData::add_residual(int q_idx, int irrep, double r) -{ - residuals_[std::make_pair(q_idx, irrep)].push_back(r); -} - -std::vector DFPT_IrrepData::get_residuals(int q_idx, int irrep) const -{ - std::map, std::vector>::const_iterator it - = residuals_.find(std::make_pair(q_idx, irrep)); - return it != residuals_.end() ? it->second : std::vector(); -} - -void DFPT_IrrepData::set_current_iter(int q_idx, int irrep, int iter) -{ - current_iter_[std::make_pair(q_idx, irrep)] = iter; -} - -int DFPT_IrrepData::get_current_iter(int q_idx, int irrep) const -{ - std::map, int>::const_iterator it - = current_iter_.find(std::make_pair(q_idx, irrep)); - return it != current_iter_.end() ? it->second : 0; -} - -} // namespace ModuleDFPT \ No newline at end of file diff --git a/source/source_pw/module_dfpt/dfpt_irrep_data.h b/source/source_pw/module_dfpt/dfpt_irrep_data.h deleted file mode 100644 index 6a716838010..00000000000 --- a/source/source_pw/module_dfpt/dfpt_irrep_data.h +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @file dfpt_irrep_data.h - * @brief Interface-adaptation layer exposing irrep-indexed DFPT data access. - * @author Mohan Chen (added on 2026-05-18) - * @note Phase 4 (DFPT wiring) interim layer: it simulates the option-2 - * signatures (an irrep dimension added to the DFPT_PW_Data storage) - * on top of the current per-q storage, so that DFPT_PW::run can drive - * a per-irrep SCF loop and be verified first. Once verified, this - * adapter is sunk into DFPT_PW_Data as its official data layer. - */ -#ifndef DFPT_IRREP_DATA_H -#define DFPT_IRREP_DATA_H - -#include "dfpt_pw_data.h" -#include -#include -#include -#include - -namespace ModuleDFPT { - -/** - * @brief Wrapper exposing irrep-indexed DFPT data access (option-2 signature). - * - * The underlying DFPT_PW_Data storage is indexed per q-point only; this - * wrapper adds the little-group irrep dimension on top, delegating the - * first-order quantities to the per-q storage and keeping a per-(q, irrep) - * convergence state that the official data layer will absorb later. - */ -class DFPT_IrrepData { -public: - /** - * @brief Construct the adapter over a DFPT_PW_Data. - * @param data underlying per-q data - */ - explicit DFPT_IrrepData(DFPT_PW_Data& data); - - /** - * @brief Number of q-points. - */ - int get_nq() const; - - /** - * @brief Number of irreps at given q-point. - * @param q_idx q-point index - */ - int get_nirr(int q_idx) const; - - /** - * @brief Representative modes of a given irrep. - * @param q_idx q-point index - * @param irrep irrep index - */ - std::vector get_irrep_modes(int q_idx, int irrep) const; - - /** - * @brief Irrep-indexed first-order wave-function coefficients. - */ - void set_dpsi(int q_idx, int irrep, int k_idx, int band_idx, - const std::vector>& psi); - std::vector> get_dpsi(int q_idx, int irrep, int k_idx, int band_idx) const; - - /** - * @brief Irrep-indexed first-order charge density (real space). - */ - void set_drho_r(int q_idx, int irrep, int spin, const std::vector& rho); - std::vector get_drho_r(int q_idx, int irrep, int spin) const; - - /** - * @brief Irrep-indexed first-order charge density (G space). - */ - void set_drho_g(int q_idx, int irrep, int spin, const std::vector>& rho); - std::vector> get_drho_g(int q_idx, int irrep, int spin) const; - - /** - * @brief Irrep-indexed first-order potential (real space). - */ - void set_dv_r(int q_idx, int irrep, int spin, const std::vector& v); - std::vector get_dv_r(int q_idx, int irrep, int spin) const; - - /** - * @brief Per-(q, irrep) SCF convergence bookkeeping. - */ - 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; - -private: - DFPT_PW_Data& data_; - - // per-(q, irrep) SCF state; the official data layer will store these - // keyed by irrep once the adapter is sunk into DFPT_PW_Data. - std::map, bool> converged_; - std::map, std::vector> residuals_; - std::map, int> current_iter_; -}; - -} // namespace ModuleDFPT - -#endif // DFPT_IRREP_DATA_H diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 105e16ae7e3..efd1933c930 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -8,7 +8,6 @@ #include "dfpt_pw.h" #include "dfpt_pw_data.h" -#include "dfpt_irrep_data.h" #include "dfpt_pert.h" #include "dfpt_stern.h" #include "dfpt_rho.h" @@ -460,7 +459,10 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { // 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) { - data_.set_current_iter(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) if (jprobe && iter == 0) { std::vector> trial(pw_rho_->npw, @@ -738,14 +740,12 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } rho_.mix_drho(q_idx, data_); residual = rho_.get_residual(q_idx, data_); - data_.add_residual(residual); if (dbg) { std::cout << "DBG iter=" << iter << " residual=" << residual << " conv_thr=" << conv_thr_ << std::endl; } converged = (residual < conv_thr_); } - data_.set_converged(converged); // 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) @@ -1156,7 +1156,6 @@ void DFPT_PW::Impl::solve_efield_resp(int q_idx) { std::complex(0.0, 0.0))); bool converged = false; for (int iter = 0; iter < max_iter_ && !converged; ++iter) { - data_.set_current_iter(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)); @@ -1556,7 +1555,6 @@ void DFPT_PW::Impl::aleg_crosscheck(int q_idx) { void DFPT_PW::run() { const int nq = pimpl_->qlist_.get_nq(); - DFPT_IrrepData irrep_data(pimpl_->data_); for (int q_idx = 0; q_idx < nq; ++q_idx) { // Special handling for q=0 (uniform electric field responses): // The standard position operator r is ill-defined in periodic systems. @@ -1589,12 +1587,20 @@ void DFPT_PW::run() { // 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. - const int nirr = irrep_data.get_nirr(q_idx); + // 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) { - irrep_data.set_converged(q_idx, irrep, false); - irrep_data.set_current_iter(q_idx, irrep, 0); - while (!irrep_data.get_converged(q_idx, irrep) - && irrep_data.get_current_iter(q_idx, irrep) < pimpl_->max_iter_) { + 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 @@ -1618,12 +1624,16 @@ void DFPT_PW::run() { pimpl_->data_); } } - irrep_data.add_residual(q_idx, irrep, worst); + 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 - irrep_data.add_residual(q_idx, irrep, 0.0); + pimpl_->data_.add_residual(q_idx, irrep, 0.0); + pimpl_->data_.set_converged(q_idx, irrep, true); } - irrep_data.set_converged(q_idx, irrep, true); + pimpl_->data_.set_current_iter( + q_idx, irrep, pimpl_->data_.get_current_iter(q_idx, irrep) + 1); } } diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index c2f86a41c15..e3fe114bcda 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -192,10 +192,31 @@ std::vector> DFPT_PW_Data::get_dpsi(int q_idx, int k_idx, i 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) { @@ -376,7 +397,9 @@ void DFPT_PW_Data::deallocate_memory() { 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 21b57bca248..1e244116362 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -14,6 +14,8 @@ #include "source_base/vector3.h" #include "source_psi/psi.h" #include "source_cell/qlist.h" +#include +#include #include #include @@ -53,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; @@ -127,13 +128,19 @@ 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_; } - - void add_residual(double r) { residuals_.push_back(r); } - std::vector get_residuals() const { return residuals_; } + + /// 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; /// DFT+U interface reservation (U0): /// the DFPT modules never read global input state directly; the esolver @@ -248,9 +255,11 @@ class DFPT_PW_Data { 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/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index 4320d046c16..eb16ac1c1a7 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -4,10 +4,9 @@ abacus_disable_feature_definitions(__ROCM) abacus_disable_feature_definitions(__EXX) AddTest( - TARGET MODULE_DFPT_irrep_data_test + TARGET MODULE_DFPT_pw_data_test LIBS parameter base device symmetry - SOURCES dfpt_irrep_data_test.cpp - ../dfpt_irrep_data.cpp + SOURCES dfpt_pw_data_test.cpp ../dfpt_pw_data.cpp ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp @@ -34,7 +33,6 @@ AddTest( SOURCES dfpt_pw_run_test.cpp ../dfpt_pw.cpp ../dfpt_pw_data.cpp - ../dfpt_irrep_data.cpp ../dfpt_pert.cpp ../dfpt_kq_basis.cpp ../dfpt_stern.cpp diff --git a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp similarity index 68% rename from source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp rename to source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp index c55d6d5964b..6c564ec1d68 100644 --- a/source/source_pw/module_dfpt/test/dfpt_irrep_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp @@ -13,7 +13,6 @@ #include "source_base/mathzone.h" #include "source_base/parallel_global.h" #include "source_base/global_variable.h" -#include "source_pw/module_dfpt/dfpt_irrep_data.h" #include "source_pw/module_dfpt/dfpt_pw_data.h" pseudo::pseudo() @@ -52,18 +51,19 @@ Sep_Cell::Sep_Cell() noexcept {} Sep_Cell::~Sep_Cell() noexcept {} /************************************************ - * unit test of DFPT_IrrepData (Phase 4 wiring) + * unit test of DFPT_PW_Data (Phase 4 wiring; B4 + * absorbed the retired DFPT_IrrepData adapter) ***********************************************/ /** * - Tested Functions: - * - DFPT_IrrepData::get_nq() / get_nirr() / get_irrep_modes() - * - delegates to the underlying QList irrep data - * - DFPT_IrrepData::set/get_dpsi, drho_r, drho_g, dv_r - * - irrep-indexed accessors forward to the per-q DFPT_PW_Data - * - DFPT_IrrepData::set/get_converged, add/get_residuals, + * - 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 bookkeeping + * - per-(q, irrep) SCF ledger (B4: sunk from DFPT_IrrepData) */ // abbreviated from module_symmetry/test/symm_test.cpp and klist_test.cpp @@ -93,7 +93,7 @@ std::vector stru_lib{stru_{1, {0., 0., 0.}, }}}}}; -class DFPT_IrrepDataTest : public testing::Test +class DFPT_PW_DataTest : public testing::Test { protected: ModuleCell::QList qlist; @@ -131,7 +131,7 @@ class DFPT_IrrepDataTest : public testing::Test 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++) + 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]); @@ -181,18 +181,16 @@ class DFPT_IrrepDataTest : public testing::Test } }; -TEST_F(DFPT_IrrepDataTest, DelegatesToQList) +TEST_F(DFPT_PW_DataTest, DelegatesToQList) { init_qlist(); - ModuleDFPT::DFPT_IrrepData irrep_data(data); - - EXPECT_EQ(irrep_data.get_nq(), qlist.get_nq()); - EXPECT_EQ(irrep_data.get_nq(), 4); - for (int q_idx = 0; q_idx < irrep_data.get_nq(); ++q_idx) + 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(irrep_data.get_nirr(q_idx), 1); - EXPECT_TRUE(irrep_data.get_irrep_modes(q_idx, 0).empty()); + 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 @@ -203,85 +201,80 @@ TEST_F(DFPT_IrrepDataTest, DelegatesToQList) clear_qlist(); } -TEST_F(DFPT_IrrepDataTest, IrrepIndexedAccessorsAreBoundSafe) +TEST_F(DFPT_PW_DataTest, AccessorsAreBoundSafe) { init_qlist(); - ModuleDFPT::DFPT_IrrepData irrep_data(data); - const int nq = irrep_data.get_nq(); - const int nirr = irrep_data.get_nirr(0); + const int nq = data.get_nq(); // out-of-range access must be safe and return empty containers - EXPECT_TRUE(irrep_data.get_irrep_modes(-1, 0).empty()); - EXPECT_TRUE(irrep_data.get_irrep_modes(nq, 0).empty()); - EXPECT_TRUE(irrep_data.get_dpsi(-1, 0, 0, 0).empty()); - EXPECT_TRUE(irrep_data.get_drho_r(0, 5, 0).empty()); - EXPECT_TRUE(irrep_data.get_drho_g(0, 5, 0).empty()); - EXPECT_TRUE(irrep_data.get_dv_r(0, 5, 0).empty()); - - (void)nirr; + 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_IrrepDataTest, SetterRoundTripViaWrapper) +TEST_F(DFPT_PW_DataTest, SetterRoundTrip) { init_qlist(); - ModuleDFPT::DFPT_IrrepData irrep_data(data); - - // dpsi / drho / dv storage went live with C1 (dv) and C3 (drho): the - // wrapper must forward the irrep-indexed calls to the per-q storage - // slot and reads must return what was written + // 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)); - irrep_data.set_dpsi(0, 0, 0, 0, psi); + data.set_dpsi(0, 0, 0, psi); std::vector rho(2, 3.0); - irrep_data.set_drho_r(0, 0, 0, rho); - irrep_data.set_drho_g(0, 0, 0, std::vector>(2, std::complex(1.0, 0.0))); - irrep_data.set_dv_r(0, 0, 0, rho); + 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); - // the wrapper reads the same slot the setter wrote through - EXPECT_FALSE(irrep_data.get_dpsi(0, 0, 0, 0).empty()); - EXPECT_FALSE(irrep_data.get_drho_r(0, 0, 0).empty()); - EXPECT_FALSE(irrep_data.get_drho_g(0, 0, 0).empty()); - EXPECT_FALSE(irrep_data.get_dv_r(0, 0, 0).empty()); + 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_IrrepDataTest, PerIrrepScfBookkeeping) +TEST_F(DFPT_PW_DataTest, PerIrrepScfLedger) { init_qlist(); - ModuleDFPT::DFPT_IrrepData irrep_data(data); - const int nirr = irrep_data.get_nirr(0); - - // bookkeeping must be independent per (q_idx, irrep) - irrep_data.set_converged(0, 0, false); - irrep_data.set_converged(1, 0, true); - EXPECT_FALSE(irrep_data.get_converged(0, 0)); - EXPECT_TRUE(irrep_data.get_converged(1, 0)); - - irrep_data.add_residual(0, 0, 1e-3); - irrep_data.add_residual(0, 0, 2e-4); - irrep_data.add_residual(1, 0, 9e-5); - EXPECT_EQ(irrep_data.get_residuals(0, 0).size(), 2); - EXPECT_EQ(irrep_data.get_residuals(1, 0).size(), 1); - EXPECT_NEAR(irrep_data.get_residuals(0, 0)[1], 2e-4, 1e-12); - - irrep_data.set_current_iter(0, 0, 3); - EXPECT_EQ(irrep_data.get_current_iter(0, 0), 3); - EXPECT_EQ(irrep_data.get_current_iter(1, 0), 0); // untouched key defaults to 0 - - for (int irrep = 0; irrep < nirr; ++irrep) - { - EXPECT_FALSE(irrep_data.get_converged(0, irrep)); - } + // 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_IrrepDataTest, DftuReservationWithNullProvider) +TEST_F(DFPT_PW_DataTest, DftuReservationWithNullProvider) { init_qlist(); From dbaa0e8ec2ca1e8f8756197e4b8dec5ba5a6de9e Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 25 Aug 2026 11:17:45 +0800 Subject: [PATCH 38/50] DFPT: retire the B-phase validation instrumentation (net -977 lines) - Deleted (acceptance complete): PTCHK gauge/term2/HF-channel probes and the drho_dfpt.dat dump; the DYNCHK family (term2/d2gate/d2k/d2/ion/ele/elei and the DYNCHK4 double-zheev comparison); MDBG binary dumps (x2); JPROBE + JPROBE_NOXC (B3 acceptance done, delete as planned); OCCCHK incl. the dbg_miss label analysis and the empty_kq_/empty_kq_eig_ companion storage; XB; BPT incl. the want_empty projector expansion; NOSC; XCS/NOXC (v_sc assembly simplified to the knob-free path); DKCHK; YCHK; D2MID (include_middle sunk to literal true, q-independence settled); ALEG + PTCROSS (the whole aleg_crosscheck method); STARDBG; Q0DBG. Dead accumulators (d2sum_loc/nl, cross_k) and the now-purposeless / includes removed with them. - Kept: DFPT_DEBUG (SCF residual tracing + posresp tracking, the B3/B4 acceptance instrument and routine convergence diagnostics) and the B3 calibration knobs DFPT_MIX_BETA / DFPT_MIX_TYPE / DFPT_KERKER_A2 (documented in the DFPT_Rho::init comment). - Behavior-preserving: every deleted probe was env-gated off by default; include_middle and want_empty defaults equal the sunk values. - Verification: OMP_NUM_THREADS=1 ctest -R 'MODULE_CELL_klist_test$| MODULE_CELL_reciprocal_grid_test|MODULE_CELL_qlist_test| MODULE_CELL_little_group_test|MODULE_DFPT' -> 12/12; serial suites pert 8 / phon 12 / q0 6 / rho 8 pass; end-to-end L-point default-config smoke (abacus_pw_para v3.11.0-beta8) reproduces the reference frequencies bit-consistently (100.487828/100.487829/380.413847/402.109158/ 485.931988/485.931988 cm^-1). Governance --staged clean except the expected no-docs-needed WARNING (internal env probes, no INPUT change). --- .../module_dfpt/PLAN_dfpt_implementation.md | 21 +- source/source_pw/module_dfpt/dfpt_phon.cpp | 127 +-- source/source_pw/module_dfpt/dfpt_pw.cpp | 822 +----------------- source/source_pw/module_dfpt/dfpt_q0.cpp | 52 -- 4 files changed, 30 insertions(+), 992 deletions(-) diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md index de86a17be8b..24b9c1a48fd 100644 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md @@ -578,7 +578,22 @@ - 验证:ctest 12/12(pw_data_test 顶替 irrep_data_test 槽位); 4 串行套件 pert 8/phon 12/q0 6/rho 8;端到端 L 点默认配置 冒烟逐位复现参考频率(见提交) - - [ ] 插桩清理评审(B0/B3 后统一):PTCHK/DYNCHK(+2/4/XB)/MDBG/JPROBE/ - VKBCHK/VKBEL/OCCCHK/XB/ZDBG/BPT/NOSC/D2MID/DFPT_MIX_BETA/drho dump - (JPROBE 留 B3 验收后删) + - [x] 插桩清理评审(B0/B3 后统一,2026-08-25 完成) + - 已删(验收全毕的设计期仪器):PTCHK(规范检查 + 8 带 term2 交叉 + + HF/de_code 通道定位)、DYNCHK 全家(term2/d2gate/d2k/d2/ion/ele/ + elei/DYNCHK4 双 zheev 对照)、MDBG(drho_iters 二进制倾倒 ×2)、 + JPROBE+JPROBE_NOXC(B3 验收后按计划删)、OCCCHK(含 dbg_miss 标签 + 分析与 empty_kq_/empty_kq_eig_ 伴生存储)、XB、BPT(含 want_empty + 投影子扩张)、NOSC、XCS/NOXC(v_sc 组装简化为无旋钮路径)、DKCHK、 + YCHK、D2MID(include_middle 收编为字面 true,q 无关性已定案)、 + ALEG+PTCROSS(aleg_crosscheck 方法整体删除)、STARDBG、Q0DBG、 + drho_dfpt.dat dump;连带清理死累加器(d2sum_loc/nl、cross_k)与 + 失用途头文件(pw.cpp 的 /) + - 保留:DFPT_DEBUG(solve 循环残差轨迹 + posresp 追踪,B3/B4 验收 + 仪器,日常收敛诊断);DFPT_MIX_BETA/DFPT_MIX_TYPE/DFPT_KERKER_A2 + (B3 设计期校准旋钮,init 注释已文档化) + - 验证:ctest 12/12、串行 pert 8/phon 12/q0 6/rho 8;默认路径行为 + 不变(删除项全部 env 门控默认关;include_middle/want_empty 默认值 + 与收编值一致)——端到端 L 点默认配置冒烟逐位复现参考频率 + (100.487828/100.487829/380.413847/402.109158/485.931988×2) - [ ] A irrep 分解(保留接口,工程验证完成后立项) diff --git a/source/source_pw/module_dfpt/dfpt_phon.cpp b/source/source_pw/module_dfpt/dfpt_phon.cpp index c8fb24b1c1b..9807307113b 100644 --- a/source/source_pw/module_dfpt/dfpt_phon.cpp +++ b/source/source_pw/module_dfpt/dfpt_phon.cpp @@ -380,16 +380,9 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, // cross terms by the variational identity, so only the bare // external perturbation appears here pert_->build_dv(q_idx, iat, idir, data); - const bool dbg2 = (getenv("DFPT_DEBUG") != nullptr); - const bool xbk = (getenv("DFPT_XB") != nullptr - && (rowb == 0 || rowb == 6) - && (cola == 0 || cola == 3 || cola == 1 - || cola == 6)); std::complex cross(0.0, 0.0); - std::vector> cross_k; for (int ik = 0; ik < nk; ++ik) { pert_->apply_dv(q_idx, ik, psi, data); - std::complex cross_k_sum(0.0, 0.0); for (int ib = 0; ib < nbands; ++ib) { if (!dfpt_band_occupied(wg, ik, ib)) { continue; @@ -400,41 +393,17 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, continue; } std::complex dot(0.0, 0.0); - double nsol = 0.0; - double nrhs = 0.0; for (size_t i = 0; i < sol.size(); ++i) { dot += std::conj(sol[i]) * rhs[i]; - nsol += std::norm(sol[i]); - nrhs += std::norm(rhs[i]); } cross += wg(ik, ib) * dot; - cross_k_sum += wg(ik, ib) * dot; - if (xbk) { - std::cout << "XB rowb=" << rowb << " cola=" << cola - << " ik=" << ik << " ib=" << ib - << " w=" << wg(ik, ib) - << " dot=(" << dot.real() << "," << dot.imag() << ")" - << " |sol|=" << std::sqrt(nsol) - << " |rhs|=" << std::sqrt(nrhs) - << std::endl; - } } - cross_k.push_back(cross_k_sum); } 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; - if (dbg2) { - std::cout << "DYNCHK term2 rowb=" << rowb << " cola=" << cola - << " cross=" << cross.real() - << " imag=" << cross.imag(); - for (size_t ikp = 0; ikp < cross_k.size(); ++ikp) { - std::cout << " k" << ikp << "=" << cross_k[ikp].real(); - } - std::cout << std::endl; - } // ---- same-atom anharmonic term ---- // QE ground truth (dynmat_us.f90 + phq_init.f90): the mixed @@ -450,12 +419,10 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, // 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); - const char* d2mid_env = getenv("DFPT_D2MID"); - const bool include_middle = !(d2mid_env != nullptr && d2mid_env[0] == '0'); - if (dbg2 && iat == atom_idx && cola == rowb) { - std::cout << "DYNCHK d2gate rowb=" << rowb - << " mid=" << (include_middle ? 1 : 0) << std::endl; - } + // 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); @@ -464,8 +431,6 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, } std::vector>> chi; std::complex d2sum(0.0, 0.0); - std::complex d2sum_loc(0.0, 0.0); - std::complex d2sum_nl(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)); @@ -495,21 +460,11 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, std::fill(x_r.begin(), x_r.end(), std::complex(0.0, 0.0)); } std::complex expect(0.0, 0.0); - std::complex expect_loc(0.0, 0.0); - std::complex expect_nl(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]; - expect_loc += std::conj(u_r[ir]) * u_r[ir] * dv2_r[ir]; - expect_nl += std::conj(u_r[ir]) * x_r[ir]; } d2sum += wg(ik, ib) * expect / static_cast(pw_rho_->nxyz); - d2sum_loc += wg(ik, ib) * expect_loc / static_cast(pw_rho_->nxyz); - d2sum_nl += wg(ik, ib) * expect_nl / static_cast(pw_rho_->nxyz); - if (dbg2 && ib == 0) { - std::cout << "DYNCHK d2k rowb=" << rowb << " cola=" << cola - << " ik=" << ik << " acc=" << d2sum.real() << std::endl; - } } } const double inv_m @@ -518,13 +473,6 @@ void DFPT_Phon::accumulate_electron(int q_idx, int atom_idx, int dir, if (cola != rowb) { dynmat_accum_(cola, rowb) += std::conj(d2sum) * inv_m; } - if (dbg2) { - std::cout << "DYNCHK d2 rowb=" << rowb << " cola=" << cola - << " d2sum=" << d2sum.real() - << " loc=" << d2sum_loc.real() - << " nl=" << d2sum_nl.real() - << " imag=" << d2sum.imag() << std::endl; - } } } } @@ -554,32 +502,6 @@ void DFPT_Phon::assemble(int q_idx, DFPT_PW_Data& data) { ion_ion(data.get_qvec(q_idx), dyn); } if (accum_q_ == q_idx && dynmat_accum_.nr == nat3) { - if (getenv("DFPT_DEBUG") != nullptr) { - std::cout << "DYNCHK ionic matrix (Ry/bohr^2/amu):" << std::endl; - for (int i = 0; i < nat3; ++i) { - std::cout << "DYNCHK ion row " << i << ":"; - for (int j = 0; j < nat3; ++j) { - std::cout << " " << dyn(i, j).real(); - } - std::cout << std::endl; - } - std::cout << "DYNCHK electronic accum matrix:" << std::endl; - for (int i = 0; i < nat3; ++i) { - std::cout << "DYNCHK ele row " << i << ":"; - for (int j = 0; j < nat3; ++j) { - std::cout << " " << dynmat_accum_(i, j).real(); - } - std::cout << std::endl; - } - std::cout << "DYNCHK electronic accum matrix (imag):" << std::endl; - for (int i = 0; i < nat3; ++i) { - std::cout << "DYNCHK elei row " << i << ":"; - for (int j = 0; j < nat3; ++j) { - std::cout << " " << dynmat_accum_(i, j).imag(); - } - std::cout << std::endl; - } - } for (int i = 0; i < nat3; ++i) { for (int j = 0; j < nat3; ++j) { dyn(i, j) += dynmat_accum_(i, j); @@ -620,50 +542,9 @@ void DFPT_Phon::diagonalize(int q_idx, DFPT_PW_Data& data) { int info = 0; LapackConnector::zheev('N', 'U', nat3, dyn, nat3, w.data(), work.data(), -1, rwork.data(), &info); - if (getenv("DFPT_DEBUG") != nullptr) { - std::vector> auxp(nat3 * nat3); - for (int i = 0; i < nat3; ++i) { - for (int j = 0; j < nat3; ++j) { - auxp[i * nat3 + j] = dyn(j, i); - } - } - std::cout << "DYNCHK4 pre-call dyn (logical rows):" << std::endl; - for (int i = 0; i < nat3; ++i) { - std::cout << "DYNCHK4 row " << i << ":"; - for (int j = 0; j < nat3; ++j) { - std::cout << " " << dyn(i, j); - } - std::cout << std::endl; - } - std::vector w2(nat3, 0.0); - std::vector rwork2(std::max(1, 3 * nat3 - 2), 0.0); - std::vector> wq(1); - int infoq = 0; - zheev_("N", "U", &nat3, auxp.data(), &nat3, w2.data(), wq.data(), - new int(-1), rwork2.data(), &infoq); - const int lwork2 = static_cast(wq[0].real()); - std::cout << "DYNCHK4 query lwork=" << lwork2 << " infoq=" << infoq << std::endl; - std::vector> work2(std::max(1, lwork2)); - int info2 = 0; - zheev_("N", "U", &nat3, auxp.data(), &nat3, w2.data(), work2.data(), - new int(std::max(1, lwork2)), rwork2.data(), &info2); - std::cout << "DYNCHK4 direct zheev_ info=" << info2 << " eig:"; - for (int i = 0; i < nat3; ++i) { - std::cout << " " << w2[i]; - } - std::cout << std::endl; - } 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); - if (getenv("DFPT_DEBUG") != nullptr) { - std::cout << "DYNCHK4 connector w info=" << info << " workopt=" << work.size() - << " eig:"; - for (int i = 0; i < nat3; ++i) { - std::cout << " " << w[i]; - } - std::cout << std::endl; - } // 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) diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index efd1933c930..7197f3d5682 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -17,7 +17,6 @@ #include "dfpt_hamilt_shift.h" #include "dfpt_kq_basis.h" #include "source_base/constants.h" -#include #include "source_base/global_function.h" #include #include "source_cell/qlist.h" @@ -28,7 +27,6 @@ #include #include #include -#include #include #include @@ -68,9 +66,6 @@ class DFPT_PW::Impl { ///< 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_; - ///< BPT debug: empty states at k+q on the k+q G list + their eigenvalues - std::vector>>> empty_kq_; - std::vector> empty_kq_eig_; ///< remembers the (q_idx, ik) the shifted operator was last cached at int last_q_ = -1; int last_ik_ = -1; @@ -99,14 +94,8 @@ class DFPT_PW::Impl { /// 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); design-phase - /// probe behind DFPT_ALEG for the zstar_eu cross-check + /// with the screening assembly of solve_displacement) void solve_efield_resp(int q_idx); - - /// zstar_eu cross-check of the screened Born charges (dpsi^E,scf - /// contracted with the bare dV^kappa|psi> legs) against the zstar_ue - /// form compute_born produced (DFPT_ALEG probe) - void aleg_crosscheck(int q_idx); }; DFPT_PW::DFPT_PW() : pimpl_(new Impl()) {} @@ -234,11 +223,6 @@ bool DFPT_PW::get_u_active() const { void DFPT_PW::Impl::build_occ_kq(int q_idx) { const int nk = pw_wfc_->nks; occ_kq_.assign(nk, std::vector>>()); - // BPT debug companion: empty states at k+q on the same kq ball, for the - // independent perturbation-theory cross-check of the Sternheimer solve - // (DFPT_BPT); eig pairs are (eig_(ikq, m), eig_(ik, n)) - empty_kq_.assign(nk, std::vector>>()); - empty_kq_eig_.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; @@ -312,12 +296,8 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { } const int nbands = gs_psi_.get_nbands(); - const bool want_empty = (getenv("DFPT_BPT") != nullptr); - int dbg_miss = 0; - int dbg_tot = 0; for (int m = 0; m < nbands; ++m) { - const bool occ_m = dfpt_band_occupied(wg_, ikq, m); - if (!occ_m && !want_empty) { + 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)); @@ -330,96 +310,9 @@ void DFPT_PW::Impl::build_occ_kq(int q_idx) { const auto it = jgl_of_n.find(key); if (it != jgl_of_n.end()) { state[igl] = gs_psi_(ikq, m, it->second); - } else { - ++dbg_miss; - } - ++dbg_tot; - } - if (occ_m) { - occ_kq_[ik].push_back(std::move(state)); - } else { - empty_kq_[ik].push_back(std::move(state)); - empty_kq_eig_[ik].push_back(eig_(ikq, m)); - } - } - if (getenv("DFPT_DEBUG") != nullptr) { - std::cout << "OCCCHK ik=" << ik << " ikq=" << ikq - << " dn=(" << dn_i[0] << "," << dn_i[1] << "," << dn_i[2] << ")" - << " npw_kq=" << npw_kq - << " npwk_ikq=" << pw_wfc_->npwk[ikq] - << " miss=" << dbg_miss << "/" << dbg_tot << std::endl; - if (dbg_miss > 0 && npw_kq > 0) { - std::set> kq_labels; - for (int igl = 0; igl < npw_kq; ++igl) { - const ModuleBase::Vector3 gf = kq.get_gcar(igl) * ginv; - kq_labels.insert({static_cast(std::round(gf.x)), - static_cast(std::round(gf.y)), - static_cast(std::round(gf.z))}); - } - std::set> gs_labels; - std::set gs_igs; - int ig_max = -1; - for (int jgl = 0; jgl < pw_wfc_->npwk[ikq]; ++jgl) { - const int ig = pw_wfc_->getigl2ig(ikq, jgl); - gs_igs.insert(ig); - if (ig > ig_max) { - ig_max = ig; - } - const ModuleBase::Vector3 gf - = pw_wfc_->getgcar(ikq, jgl) * ginv; - gs_labels.insert({static_cast(std::round(gf.x)), - static_cast(std::round(gf.y)), - static_cast(std::round(gf.z))}); - } - std::cout << "OCCCHK gs_unique_ig=" << gs_igs.size() - << "/" << pw_wfc_->npwk[ikq] - << " ig_max=" << ig_max - << " npw=" << pw_wfc_->npw - << " npwk_max=" << pw_wfc_->npwk_max - << std::endl; - int only_kq = 0; - std::cout << "OCCCHK labels kq=" << kq_labels.size() - << " gs=" << gs_labels.size(); - for (const auto& k : kq_labels) { - if (gs_labels.count(k) == 0) { - ++only_kq; - } - } - std::cout << " only_kq=" << only_kq << std::endl; - int shown = 0; - for (const auto& k : kq_labels) { - if (gs_labels.count(k) == 0) { - std::cout << "OCCCHK kq-only (" << k[0] << "," << k[1] << "," - << k[2] << ")"; - if (++shown >= 6) { - break; - } - } } - if (shown > 0) { - std::cout << std::endl; - } - shown = 0; - for (const auto& k : gs_labels) { - if (kq_labels.count(k) == 0) { - std::cout << "OCCCHK gs-only (" << k[0] << "," << k[1] << "," - << k[2] << ")"; - if (++shown >= 6) { - break; - } - } - } - if (shown > 0) { - std::cout << std::endl; - } - const ModuleBase::Vector3 gf0 = kq.get_gcar(0) * ginv; - std::cout << "OCCCHK kq key0 gf=(" << gf0.x << "," << gf0.y << "," - << gf0.z << ")"; - const ModuleBase::Vector3 gj0 - = pw_wfc_->getgcar(ikq, 0) * ginv; - std::cout << " ikq gf0=(" << gj0.x << "," << gj0.y << "," << gj0.z << ")" - << std::endl; } + occ_kq_[ik].push_back(std::move(state)); } } last_q_ = q_idx; @@ -446,11 +339,6 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { const int lin_max = data_.get_max_iter(); const double lin_thr = data_.get_conv_thr(); - // design-phase homogeneous probe: inject a pure A1 trial density on the - // {200}/{111} shells, drop the external perturbation from the rhs, run - // one iteration and dump the linear map output M * trial - const bool jprobe = (getenv("DFPT_JPROBE") != nullptr); - const bool jprobe_noxc = (getenv("DFPT_JPROBE_NOXC") != nullptr); bool converged = false; double residual = 0.0; @@ -464,32 +352,6 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { // outer-pass record kept by run(), and the final residual is // returned to the caller for that aggregation (B4) - if (jprobe && iter == 0) { - std::vector> trial(pw_rho_->npw, - std::complex(0.0, 0.0)); - double nrm = 0.0; - for (int ig = 0; ig < pw_rho_->npw; ++ig) { - const ModuleBase::Vector3 gc = pw_rho_->gcar[ig]; - const double g2 = gc * gc; - const int nax = (std::abs(gc.x) > 1.0e-6) - + (std::abs(gc.y) > 1.0e-6) - + (std::abs(gc.z) > 1.0e-6); - if (std::abs(g2 - 4.0) < 1.0e-9 && nax == 1) { - trial[ig] = std::complex(1.0, 0.0); - nrm += 1.0; - } else if (std::abs(g2 - 3.0) < 1.0e-9 && nax == 3) { - const double sgn = (gc.x * gc.y * gc.z > 0.0) ? 1.0 : -1.0; - trial[ig] = std::complex(0.6218, -0.6218 * sgn); - nrm += 2.0 * 0.6218 * 0.6218; - } - } - const double inv = 1.0 / std::sqrt(nrm); - for (size_t i = 0; i < trial.size(); ++i) { - trial[i] *= inv; - } - data_.set_drho_g(q_idx, 0, trial); - } - // ---- 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 @@ -504,39 +366,14 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { for (int ir = 0; ir < nrxx; ++ir) { v_sc_r[ir] = vh_r[ir]; } - const bool noxc = (getenv("DFPT_NOXC") != nullptr); - double xcs = 1.0; - if (getenv("DFPT_XCS") != nullptr) { - xcs = std::atof(getenv("DFPT_XCS")); - } - if (xc_ != nullptr && !jprobe_noxc && !noxc) { + 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] += xcs * b_r[ir]; - } - } - if (getenv("DFPT_MDBG") != nullptr) { - static int vdbg_cnt = 0; - std::ofstream vf("/tmp/opencode/drho_iters/vsc_" - + std::to_string(vdbg_cnt++) + ".bin", - std::ios::binary); - for (int ir = 0; ir < nrxx; ++ir) { - vf.write(reinterpret_cast(&v_sc_r[ir]), - sizeof(std::complex)); - } - // channel split: rebuild each part for the same input - std::vector> vh_only(nrxx); - pw_rho_->recip2real(dv_ha_g.data(), vh_only.data()); - std::ofstream hf("/tmp/opencode/drho_iters/vha_" - + std::to_string(vdbg_cnt - 1) + ".bin", - std::ios::binary); - for (int ir = 0; ir < nrxx; ++ir) { - hf.write(reinterpret_cast(&vh_only[ir]), - sizeof(std::complex)); + v_sc_r[ir] += b_r[ir]; } } } @@ -551,37 +388,8 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } std::cout << "DBG iter=" << iter << " |drho_in_g|=" << std::sqrt(dh) << " |v_sc_r|=" << std::sqrt(dv) << std::endl; - if (getenv("DFPT_MDBG") != nullptr) { - static int dump_cnt = 0; - std::ofstream df("/tmp/opencode/drho_iters/it" - + std::to_string(dump_cnt++) + ".bin", - std::ios::binary); - for (int ig = 0; ig < pw_rho_->npw; ++ig) { - df.write(reinterpret_cast(&drho_in_g[ig]), - sizeof(std::complex)); - } - static bool dumped_g = false; - if (!dumped_g) { - dumped_g = true; - std::ofstream gf("/tmp/opencode/drho_iters/gcar.bin", - std::ios::binary); - for (int ig = 0; ig < pw_rho_->npw; ++ig) { - const double gx = pw_rho_->gcar[ig].x; - const double gy = pw_rho_->gcar[ig].y; - const double gz = pw_rho_->gcar[ig].z; - gf.write(reinterpret_cast(&gx), sizeof(double)); - gf.write(reinterpret_cast(&gy), sizeof(double)); - gf.write(reinterpret_cast(&gz), sizeof(double)); - } - } - } } } - // DFPT_NOSC: zero the screened part to isolate the bare Sternheimer - // (design-phase A/B knob for term2 debugging) - if (getenv("DFPT_NOSC") != nullptr) { - std::fill(v_sc_r.begin(), v_sc_r.end(), std::complex(0.0, 0.0)); - } v_sc_r_last = v_sc_r; // ---- 2. Sternheimer solve of every occupied (k, band) @@ -644,16 +452,9 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { } continue; } - // b = -(dV_ext + dV_sc)|psi_n>; the homogeneous probe keeps - // only the screening part to isolate the linear map M - if (jprobe) { - for (size_t i = 0; i < rhs.size(); ++i) { - rhs[i] = -dv_sc[ib][i]; - } - } else { - for (size_t i = 0; i < rhs.size(); ++i) { - rhs[i] = -(rhs[i] + dv_sc[ib][i]); - } + // 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; @@ -673,71 +474,11 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { << std::endl; } data_.set_dpsi(q_idx, ik, ib, dpsi_out); - // BPT: independent PT cross-check of the solve for this band. - // Identity (exact on a complete empty manifold): - // == sum_m ||^2 / (e_m(k+q) - e_n(k)) - // rhs here is the TOTAL (ext+sc) right-hand side with the - // Sternheimer sign (rhs = -(ext+sc)); both sides use the same - // object so the sign cancels on the diagonal-identity check. - if (getenv("DFPT_BPT") != nullptr - && static_cast(empty_kq_[ik].size()) > 0 - && dpsi_out.size() == rhs.size()) { - std::complex codedot(0.0, 0.0); - double pt = 0.0; - double wsum = 0.0; - for (size_t i = 0; i < dpsi_out.size(); ++i) { - codedot += std::conj(dpsi_out[i]) * rhs[i]; - } - for (size_t im = 0; im < empty_kq_[ik].size(); ++im) { - const std::vector>& psim - = empty_kq_[ik][im]; - if (psim.size() != rhs.size()) { - continue; - } - std::complex mdot(0.0, 0.0); - for (size_t i = 0; i < rhs.size(); ++i) { - mdot += std::conj(psim[i]) * rhs[i]; - } - const double denom = empty_kq_eig_[ik][im] - eig_(ik, ib); - if (std::abs(denom) > 1.0e-10) { - pt += std::norm(mdot) / denom; - } - wsum += std::norm(mdot); - } - double nrm2 = 0.0; - for (size_t i = 0; i < rhs.size(); ++i) { - nrm2 += std::norm(rhs[i]); - } - std::cout << "BPTCHK q=" << q_idx << " iat=" << iat - << " idir=" << idir << " ik=" << ik << " ib=" << ib - << " code=(" << codedot.real() << "," << codedot.imag() << ")" - << " pt=" << pt - << " ||^2sum=" << wsum - << " |rhs|^2=" << nrm2 << std::endl; - } } } // ---- 3. first-order density and mixing rho_.compute_drho(gs_psi_, wg_, q_idx, data_); - if (jprobe && iter == 0) { - const std::vector> probe_out = data_.get_drho_g(q_idx, 0); - std::ofstream pf("/tmp/opencode/jprobe_out.bin", std::ios::binary); - for (int ig = 0; ig < pw_rho_->npw; ++ig) { - pf.write(reinterpret_cast(&probe_out[ig]), - sizeof(std::complex)); - } - std::ofstream vf("/tmp/opencode/jprobe_vsc.bin", std::ios::binary); - for (int ir = 0; ir < nrxx; ++ir) { - vf.write(reinterpret_cast(&v_sc_r[ir]), - sizeof(std::complex)); - } - pf.close(); - vf.close(); - std::cout << "JPROBE dumped, exiting" << std::endl; - std::cout.flush(); - std::exit(0); - } rho_.mix_drho(q_idx, data_); residual = rho_.get_residual(q_idx, data_); if (dbg) { @@ -761,160 +502,6 @@ double DFPT_PW::Impl::solve_displacement(int q_idx, int iat, int idir) { data_.set_dpsi_disp(iat, idir, disp); } - // design-phase validation: dump converged self-consistent drho on the - // shared real-space grid for direct comparison with finite differences - if (dbg && q_idx == 0 && iat == 0 && idir == 0) { - const std::vector> dg = data_.get_drho_g(q_idx, 0); - std::vector> dr(pw_rho_->nrxx, std::complex(0.0, 0.0)); - if (static_cast(dg.size()) == pw_rho_->npw) { - pw_rho_->recip2real(dg.data(), dr.data()); - } - std::ofstream df("/tmp/opencode/drho_dfpt.dat"); - df << pw_rho_->nx << " " << pw_rho_->ny << " " << pw_rho_->nz << "\n"; - for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { - df << dr[ir].real() << " " << dr[ir].imag() << "\n"; - } - } - // design-phase validation: 8-band perturbation-theory term2 cross-check - // for the first displacement of the first atom (q = 0, ik = 0 only) - if (dbg && q_idx == 0 && iat == 0 && idir == 0 && nk > 0 && nbands > 4) { - // gauge check: must vanish for occupied k (Sternheimer - // gauge); a nonzero admixture pollutes term2 via occ-occ dV elements - for (int ikg = 0; ikg < nk; ++ikg) { - for (int n = 0; n < 4; ++n) { - const std::vector>& dps = data_.get_dpsi(q_idx, ikg, n); - const int npwg = gs_psi_.get_nbasis(); - if (static_cast(dps.size()) != npwg) { - continue; - } - for (int k = 0; k < 4; ++k) { - std::complex dot(0.0, 0.0); - for (int ig = 0; ig < npwg; ++ig) { - dot += std::conj(gs_psi_(ikg, k, ig)) * dps[ig]; - } - std::cout << "PTCHK gauge ik=" << ikg << " n=" << n << " k=" << k - << " =" << dot << std::endl; - } - } - } - // stash the solved dpsi (apply_dv below reuses the slots) - std::vector>> solved(nbands); - for (int ib = 0; ib < nbands; ++ib) { - solved[ib] = data_.get_dpsi(q_idx, 0, ib); - } - const int npw = gs_psi_.get_nbasis(); - // M[a][m][n] = - std::vector>>> mat( - 2, std::vector>>( - nbands, std::vector>(nbands, std::complex(0.0, 0.0)))); - for (int a = 0; a < 2; ++a) { - pert_.build_dv(q_idx, a, idir, data_); - pert_.apply_dv(q_idx, 0, gs_psi_, data_); - for (int n = 0; n < nbands; ++n) { - const std::vector> dvpsi = data_.get_dpsi(q_idx, 0, n); - if (static_cast(dvpsi.size()) != npw) { - continue; - } - for (int m = 0; m < nbands; ++m) { - std::complex dot(0.0, 0.0); - for (int ig = 0; ig < npw; ++ig) { - dot += std::conj(gs_psi_(0, m, ig)) * dvpsi[ig]; - } - mat[a][m][n] = dot; - } - } - } - // PT term2 with the 4 empty bands only, b = atom 0 (solved dir) - for (int a = 0; a < 2; ++a) { - std::complex pt(0.0, 0.0); - for (int n = 0; n < 4; ++n) { - const double w = wg_(0, n); - if (w < 1.0e-8) { - continue; - } - for (int m = 4; m < nbands; ++m) { - pt += w * std::conj(mat[a][m][n]) * mat[0][m][n] - / (eig_(0, n) - eig_(0, m)); - } - } - std::cout << "PTCHK term2(a=" << a << ",b=0) PT-4empty=" << 2.0 * pt << std::endl; - } - // element dump: M[a][m][n] for m empty, n occupied - for (int a = 0; a < 2; ++a) { - for (int m = 4; m < nbands; ++m) { - for (int n = 0; n < 4; ++n) { - std::cout << "PTCHK M a=" << a << " m=" << m << " n=" << n - << " (" << mat[a][m][n].real() << "," << mat[a][m][n].imag() << ")" - << std::endl; - } - } - } - // solved-dpsi band projections - for (int n = 0; n < 4; ++n) { - for (int m = 0; m < nbands; ++m) { - std::complex dot(0.0, 0.0); - for (int ig = 0; ig < npw; ++ig) { - dot += std::conj(gs_psi_(0, m, ig)) * solved[n][ig]; - } - std::cout << "PTCHK proj n=" << n << " m=" << m - << " =(" << dot.real() << "," << dot.imag() << ")" - << std::endl; - } - } - // Hellmann-Feynman check targets: vs FD of eps_n - for (int a = 0; a < 2; ++a) { - for (int n = 0; n < 4; ++n) { - std::cout << "PTCHK HF a=" << a << " n=" << n - << " =" << mat[a][n][n] << std::endl; - } - } - // design-phase validation: rebuild the converged screened potential - // and compare de_code(n) = + against FD eigenvalue - // derivatives (localizes response errors in the screening channel) - { - const std::vector> dg_in = data_.get_drho_g(q_idx, 0); - if (static_cast(dg_in.size()) == pw_rho_->npw) { - std::vector> vha_g; - rho_.v_hartree_q(q_cart, dg_in, vha_g); - std::vector> v_sc_r2(pw_rho_->nrxx, std::complex(0.0, 0.0)); - std::vector> vh_r(pw_rho_->nrxx); - pw_rho_->recip2real(vha_g.data(), vh_r.data()); - std::vector> vx_r; - if (xc_ != nullptr) { - std::vector> ar(pw_rho_->nrxx); - pw_rho_->recip2real(dg_in.data(), ar.data()); - xc_->apply(ar, vx_r); - } - for (int ir = 0; ir < pw_rho_->nrxx; ++ir) { - v_sc_r2[ir] = vh_r[ir]; - if (static_cast(vx_r.size()) == pw_rho_->nrxx) { - v_sc_r2[ir] += vx_r[ir]; - } - } - std::vector>> dvsc; - pert_.apply_vr(q_idx, 0, v_sc_r2, gs_psi_, q_cart, dvsc); - for (int n = 0; n < nbands; ++n) { - const std::vector>& v = dvsc[n]; - if (static_cast(v.size()) != npw) { - continue; - } - std::complex dot(0.0, 0.0); - for (int ig = 0; ig < npw; ++ig) { - dot += std::conj(gs_psi_(0, n, ig)) * v[ig]; - } - std::cout << "PTCHK de n=" << n - << " =(" << dot.real() << "," << dot.imag() << ")" - << " de_code=" << (mat[0][n][n] + dot).real() << std::endl; - } - } - } - // restore the solved dpsi - for (int ib = 0; ib < nbands; ++ib) { - if (!solved[ib].empty()) { - data_.set_dpsi(q_idx, 0, ib, solved[ib]); - } - } - } return residual; } @@ -1003,40 +590,6 @@ void DFPT_PW::Impl::solve_pos_resp(int q_idx) { } std::vector>> dvkb; pert_.build_vkb_dk(it, ia, a, gk, vkb, dvkb); - // DKCHK: analytic dk-derivative vs central difference of - // the GS-validated build_vkb (first atom with nh > 0 only) - if (getenv("DFPT_DKCHK") != nullptr && ia == 0 && ik == 0) { - const double dd = 1.0e-5; - std::vector> gk_p(npwk); - std::vector> gk_m(npwk); - for (int ig = 0; ig < npwk; ++ig) { - gk_p[ig] = gk[ig]; - gk_m[ig] = gk[ig]; - gk_p[ig][a] += dd; - gk_m[ig][a] -= dd; - } - std::vector>> vkb_p; - std::vector>> vkb_m; - pert_.build_vkb(it, ia, gk_p, vkb_p); - pert_.build_vkb(it, ia, gk_m, vkb_m); - for (int mu = 0; mu < nh; ++mu) { - std::complex ddot(0.0, 0.0); - double ndk = 0.0; - double nnum = 0.0; - for (int ig = 0; ig < npwk; ++ig) { - const std::complex num - = (vkb_p[mu][ig] - vkb_m[mu][ig]) / (2.0 * dd); - ddot += std::conj(dvkb[mu][ig]) * num; - ndk += std::norm(dvkb[mu][ig]); - nnum += std::norm(num); - } - std::cout << "DKCHK it=" << it << " a=" << a - << " mu=" << mu - << " =" << ddot - << " |dk|^2=" << ndk - << " |num|^2=" << nnum << std::endl; - } - } // dbecp_b[mu] = std::vector>> dbecp(nbands); for (int b = 0; b < nbands; ++b) { @@ -1069,7 +622,6 @@ void DFPT_PW::Impl::solve_pos_resp(int q_idx) { } } // solve (H - eps_v) Y = -(i/tpiba) vel for every occupied band - const bool ychk = (getenv("DFPT_YCHK") != nullptr); for (int ib = 0; ib < nbands; ++ib) { if (!dfpt_band_occupied(wg_, ik, ib)) { continue; @@ -1089,38 +641,6 @@ void DFPT_PW::Impl::solve_pos_resp(int q_idx) { << " ib=" << ib << " eps=" << eig_(ik, ib) << " res=" << res << std::endl; } - if (ychk && npwk > 0) { - // eigen-projection identity: must equal the - // velocity-form matrix element -i / - // (tpiba (eps_m - eps_v)) for every nondegenerate - // conduction m (pos_matrix cross-check) - for (int m = 0; m < nbands; ++m) { - if (dfpt_band_occupied(wg_, ik, m)) { - continue; - } - const double de = eig_(ik, m) - eig_(ik, ib); - if (std::abs(de) < 1.0e-8) { - continue; - } - std::complex pdot(0.0, 0.0); - std::complex ydot(0.0, 0.0); - for (int ig = 0; ig < npwk; ++ig) { - pdot += std::conj(gs_psi_(ik, m, ig)) * vel[ib][ig]; - if (ig < static_cast(yvec[ik][ib].size())) { - ydot += std::conj(gs_psi_(ik, m, ig)) - * yvec[ik][ib][ig]; - } - } - const std::complex expect - = std::complex(0.0, -1.0) * pdot - / (tpiba * de); - std::cout << "YCHK a=" << a << " ik=" << ik - << " v=" << ib << " m=" << m - << " =(" << ydot.real() << "," << ydot.imag() << ")" - << " X_mv=(" << expect.real() << "," << expect.imag() << ")" - << " res=" << res << std::endl; - } - } } } data_.set_pos_resp(a, yvec); @@ -1230,329 +750,6 @@ void DFPT_PW::Impl::solve_efield_resp(int q_idx) { } } -void DFPT_PW::Impl::aleg_crosscheck(int q_idx) { - // zstar_eu cross-check (QE zstar_eu.f90): - // Z*(E,Us)_iat(a, d) = zion delta_ad - // - 2 sum_k w_k Re sum_v - // with the bare kappa leg from the same build_dv/apply_dv pair the - // displacement solves consumed. Printed against the plain (no star - // rotation) zstar_ue form from the dpsi_disp stash and the stored - // star-rotated Born charges, plus the SCF dielectric tensor - // eps = 1 - (16 pi / omega) sum_k wg Re - // which is the same contraction compute_eps produces in production. - // A == B incriminates - // a shared operand; A clean incriminates the compute_born contraction. - const int nat = ucell_->nat; - const int nk = gs_psi_.get_nk(); - const int nbands = gs_psi_.get_nbands(); - std::vector>>>> de(3); - std::vector>>>> yr(3); - for (int a = 0; a < 3; ++a) { - de[a] = data_.get_dpsi_efield(a); - yr[a] = data_.get_pos_resp(a); - if (static_cast(de[a].size()) != nk - || static_cast(yr[a].size()) != nk) { - std::cout << "ALEG: missing E/position response stashes" << std::endl; - return; - } - } - // A-leg: plain k sum with the full wg weight (QE form). Complex - // accumulation: a spurious relative phase between the E and kappa legs - // cancels in the EE and kappa-kappa channels but rotates this cross - // channel, so Im(z) vs Re(z) is the phase detector (ASR: |z| = zion/2) - std::vector za(nat, ModuleBase::matrix(3, 3, true)); - std::vector>> za_c( - nat, std::vector>(9, std::complex(0.0, 0.0))); - for (int iat = 0; iat < nat; ++iat) { - for (int idir = 0; idir < 3; ++idir) { - pert_.build_dv(q_idx, iat, idir, data_); - for (int ik = 0; ik < nk; ++ik) { - if (static_cast(occ_kq_.size()) <= ik || occ_kq_[ik].empty()) { - continue; - } - pert_.apply_dv(q_idx, ik, gs_psi_, data_); - for (int v = 0; v < nbands; ++v) { - if (!dfpt_band_occupied(wg_, ik, v)) { - continue; - } - const std::vector> b - = data_.get_dpsi(q_idx, ik, v); - const int npw = static_cast(b.size()); - if (npw <= 0) { - continue; - } - for (int a = 0; a < 3; ++a) { - if (static_cast(de[a][ik][v].size()) != npw) { - continue; - } - std::complex dot(0.0, 0.0); - for (int ig = 0; ig < npw; ++ig) { - dot += std::conj(de[a][ik][v][ig]) * b[ig]; - } - za[iat](a, idir) += wg_(ik, v) * dot.real(); - za_c[iat][3 * a + idir] += wg_(ik, v) * dot; - if (a == 0 && idir == 0 && ik < 3) { - std::cout << "ALEG-K A ik=" << ik << " v=" << v - << " z=(" << dot.real() << "," << dot.imag() << ")" - << " wg=" << wg_(ik, v) << std::endl; - } - } - } - } - } - } - // plain B-leg from the displacement stashes (same sum, no stars) - std::vector zb(nat, ModuleBase::matrix(3, 3, true)); - for (int iat = 0; iat < nat; ++iat) { - for (int idir = 0; idir < 3; ++idir) { - 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; - } - const int npw = static_cast(disp[ik][v].size()); - if (npw <= 0) { - continue; - } - 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]; - } - zb[iat](a, idir) += wg_(ik, v) * dot.real(); - if (a == 0 && idir == 0 && ik < 3) { - std::cout << "ALEG-K B ik=" << ik << " v=" << v - << " z=(" << dot.real() << "," << dot.imag() << ")" - << " wg=" << wg_(ik, v) << std::endl; - } - } - } - } - } - } - // SCF dielectric tensor (complex accumulation: Im(chi) = phase detector) - ModuleBase::matrix eps(3, 3, true); - std::vector> eps_c(9, std::complex(0.0, 0.0)); - for (int ik = 0; ik < nk; ++ik) { - for (int v = 0; v < nbands; ++v) { - if (!dfpt_band_occupied(wg_, ik, v)) { - continue; - } - 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]; - } - eps(a, b) += wg_(ik, v) * dot.real(); - eps_c[3 * a + b] += wg_(ik, v) * dot; - } - } - } - } - // PTCROSS: bare cross-form operator diagnostic. A fresh bare E-leg - // solve x = M^-1(-Y^0) (rhs fully known, no screening) is contracted - // with the bare kappa leg b^kappa: must equal the spectral sum - // over the available empty bands -/(eps_m-eps_v); the - // mismatch beyond that band space measures what the truncated - // manifold misses in the cross channel that diagonal norm checks - // cannot see. The kappa-side solve x^kappa = M^-1(-b) gives the - // hermiticity mirror = conj(). - if (getenv("DFPT_PTCROSS") != nullptr) { - const ModuleBase::Vector3 q_cart_p - = data_.get_qvec(q_idx) * ucell_->G; - const int ikmax = std::min(nk, 3); - for (int idir = 0; idir < 3; ++idir) { - pert_.build_dv(q_idx, 0, idir, data_); - for (int ik = 0; ik < ikmax; ++ik) { - if (static_cast(occ_kq_.size()) <= ik || occ_kq_[ik].empty()) { - continue; - } - pert_.apply_dv(q_idx, ik, gs_psi_, data_); - if (last_q_ != q_idx || last_ik_ != ik) { - hamilt_->set_context(q_cart_p, ik); - last_q_ = q_idx; - last_ik_ = ik; - } - for (int v = 0; v < nbands; ++v) { - if (!dfpt_band_occupied(wg_, ik, v)) { - continue; - } - const int npw = static_cast(yr[0][ik][v].size()); - if (npw <= 0) { - continue; - } - const std::vector> b - = data_.get_dpsi(q_idx, ik, v); - if (static_cast(b.size()) != npw) { - continue; - } - // empty-band overlaps at this k - std::vector> myv; - std::vector> mbv; - std::vector dev; - double wsum = 0.0; - for (int m = 0; m < nbands; ++m) { - if (dfpt_band_occupied(wg_, ik, m)) { - continue; - } - const double de = eig_(ik, m) - eig_(ik, v); - if (std::abs(de) < 1.0e-8) { - continue; - } - std::complex my(0.0, 0.0); - std::complex mb(0.0, 0.0); - for (int i = 0; i < npw; ++i) { - my += std::conj(gs_psi_(ik, m, i)) * yr[0][ik][v][i]; - mb += std::conj(gs_psi_(ik, m, i)) * b[i]; - } - myv.push_back(my); - mbv.push_back(mb); - dev.push_back(de); - wsum += std::norm(my); - } - // E-side solve: (H - eps_v) x = -Y^0_v - std::vector> rhsE(npw); - for (int i = 0; i < npw; ++i) { - rhsE[i] = -yr[0][ik][v][i]; - } - hamilt_->set_shift(eig_(ik, v)); - std::vector> xE; - double resE = 0.0; - stern_.solve(*hamilt_, occ_kq_[ik], rhsE, - data_.get_max_iter(), data_.get_conv_thr(), - xE, resE); - // kappa-side solve: (H - eps_v) xk = -b - std::vector> rhsK(npw); - for (int i = 0; i < npw; ++i) { - rhsK[i] = -b[i]; - } - hamilt_->set_shift(eig_(ik, v)); - std::vector> xK; - double resK = 0.0; - stern_.solve(*hamilt_, occ_kq_[ik], rhsK, - data_.get_max_iter(), data_.get_conv_thr(), - xK, resK); - std::complex crossE(0.0, 0.0); - std::complex crossK(0.0, 0.0); - std::complex diagE(0.0, 0.0); - double pt_cross = 0.0; - double pt_diagE = 0.0; - double pt_diagK = 0.0; - for (int i = 0; i < npw; ++i) { - if (static_cast(xE.size()) == npw) { - crossE += std::conj(xE[i]) * b[i]; - diagE += std::conj(xE[i]) * yr[0][ik][v][i]; - } - if (static_cast(xK.size()) == npw) { - crossK += std::conj(xK[i]) * yr[0][ik][v][i]; - } - } - for (size_t im = 0; im < myv.size(); ++im) { - pt_cross += (-std::conj(myv[im]) * mbv[im] - / dev[im]).real(); - pt_diagE += -std::norm(myv[im]) / dev[im]; - pt_diagK += -std::norm(mbv[im]) / dev[im]; - } - std::cout << "PTCROSS d=" << idir << " ik=" << ik - << " v=" << v - << " crossE=(" << crossE.real() << "," - << crossE.imag() << ")" - << " crossK=(" << crossK.real() << "," - << crossK.imag() << ")" - << " pt=" << pt_cross - << " rE=" << (pt_cross != 0.0 - ? crossE.real() / pt_cross - : 0.0) - << " diagE=(" << diagE.real() << "," - << diagE.imag() << ") ptDiagE=" << pt_diagE - << " ptDiagK=" << pt_diagK - << " wsumY=" << wsum - << " resE=" << resE << " resK=" << resK - << std::endl; - } - } - } - } - for (int a = 0; a < 3; ++a) { - for (int b = 0; b < 3; ++b) { - eps(a, b) *= -16.0 * ModuleBase::PI / ucell_->omega; - if (a == b) { - eps(a, b) += 1.0; - } - } - } - std::cout << "ALEG eps_scf:" << std::endl; - for (int a = 0; a < 3; ++a) { - std::cout << " " << eps(a, 0) << " " << eps(a, 1) << " " << eps(a, 2) - << std::endl; - } - std::cout << "ALEG chi_scf complex diag: (16pi/omega)*z per component" - << std::endl; - for (int a = 0; a < 3; ++a) { - const std::complex z = eps_c[4 * a] * (-16.0 * ModuleBase::PI - / ucell_->omega); - std::cout << " a=" << a << " z=(" << z.real() << "," << z.imag() - << ") |z|=" << std::abs(z) << std::endl; - } - const ModuleBase::matrix eps_ipa = data_.get_dielectric(); - std::cout << "ALEG eps_ipa(stored):" << std::endl; - for (int a = 0; a < 3; ++a) { - std::cout << " " << eps_ipa(a, 0) << " " << eps_ipa(a, 1) << " " - << eps_ipa(a, 2) << std::endl; - } - for (int iat = 0; iat < nat; ++iat) { - const int it = ucell_->iat2it[iat]; - const double zion = ucell_->atoms[it].ncpp.zv; - std::cout << "ALEG atom " << iat << " zion=" << zion << std::endl; - std::cout << " A(eu plain):" << std::endl; - for (int a = 0; a < 3; ++a) { - std::cout << " "; - for (int d = 0; d < 3; ++d) { - std::cout << ((a == d) ? zion : 0.0) - 2.0 * za[iat](a, d) << " "; - } - std::cout << std::endl; - } - std::cout << " A complex diag: z00..z22 (sum wg)" << std::endl; - for (int a = 0; a < 3; ++a) { - const std::complex z = za_c[iat][4 * a]; - std::cout << " z" << a << a << " = (" << z.real() << "," - << z.imag() << ") |z|=" << std::abs(z) - << " |z|/(zion/2)=" << std::abs(z) / (0.5 * zion) - << std::endl; - } - std::cout << " B(ue plain):" << std::endl; - for (int a = 0; a < 3; ++a) { - std::cout << " "; - for (int d = 0; d < 3; ++d) { - std::cout << ((a == d) ? zion : 0.0) - 2.0 * zb[iat](a, d) << " "; - } - std::cout << std::endl; - } - const ModuleBase::matrix zstar = data_.get_born(iat); - std::cout << " B(ue star-rot stored):" << std::endl; - for (int a = 0; a < 3; ++a) { - std::cout << " " << zstar(a, 0) << " " << zstar(a, 1) << " " - << zstar(a, 2) << std::endl; - } - } -} - void DFPT_PW::run() { const int nq = pimpl_->qlist_.get_nq(); for (int q_idx = 0; q_idx < nq; ++q_idx) { @@ -1644,9 +841,6 @@ void DFPT_PW::run() { if (q_idx == 0 && pimpl_->data_.get_compute_q0() && pimpl_->wired()) { pimpl_->q0_.compute_born(pimpl_->gs_psi_, pimpl_->wg_, pimpl_->eig_, pimpl_->data_); - if (getenv("DFPT_ALEG") != nullptr) { - pimpl_->aleg_crosscheck(q_idx); - } } pimpl_->phon_.assemble(q_idx, pimpl_->data_); diff --git a/source/source_pw/module_dfpt/dfpt_q0.cpp b/source/source_pw/module_dfpt/dfpt_q0.cpp index af13884bea2..25212f50f67 100644 --- a/source/source_pw/module_dfpt/dfpt_q0.cpp +++ b/source/source_pw/module_dfpt/dfpt_q0.cpp @@ -69,24 +69,7 @@ void DFPT_Q0::build_stars(int nk) { // stored list is already the full mesh, identity members only return; } - const bool stardbg = getenv("DFPT_STARDBG") != nullptr; const int nat = ucell_->nat; - if (stardbg) { - std::cout << "STARDBG nrotk=" << symm.nrotk << " nat=" << nat << std::endl; - for (int j = 0; j < symm.nrotk; ++j) { - std::cout << "STARDBG j=" << j << " kg=[" << symm.kgmatrix[j].e11 << "," - << symm.kgmatrix[j].e12 << "," << symm.kgmatrix[j].e13 << ";" - << symm.kgmatrix[j].e21 << "," << symm.kgmatrix[j].e22 << "," - << symm.kgmatrix[j].e23 << ";" << symm.kgmatrix[j].e31 << "," - << symm.kgmatrix[j].e32 << "," << symm.kgmatrix[j].e33 << "] g=[" - << symm.gmatrix[j].e11 << "," << symm.gmatrix[j].e12 << "," - << symm.gmatrix[j].e13 << ";" << symm.gmatrix[j].e21 << "," - << symm.gmatrix[j].e22 << "," << symm.gmatrix[j].e23 << ";" - << symm.gmatrix[j].e31 << "," << symm.gmatrix[j].e32 << "," - << symm.gmatrix[j].e33 << "] gt=(" << symm.gtrans[j].x << "," - << symm.gtrans[j].y << "," << symm.gtrans[j].z << ")" << std::endl; - } - } std::vector> kfolds; for (int ik = 0; ik < nk; ++ik) { kfolds.clear(); @@ -126,14 +109,6 @@ void DFPT_Q0::build_stars(int nk) { mem.cart = ModuleBase::Matrix3(krow.e11, krow.e21, krow.e31, krow.e12, krow.e22, krow.e32, krow.e13, krow.e23, krow.e33); - if (stardbg) { - std::cout << "STARDBG ik=" << ik << " j=" << j << " kp=(" << kp.x - << "," << kp.y << "," << kp.z << ") cart=[" << mem.cart.e11 - << "," << mem.cart.e12 << "," << mem.cart.e13 << ";" - << mem.cart.e21 << "," << mem.cart.e22 << "," << mem.cart.e23 - << ";" << mem.cart.e31 << "," << mem.cart.e32 << "," - << mem.cart.e33 << "]" << std::endl; - } // atom image under the paired direct-space operation mem.atom_map.assign(nat, -1); bool ok = true; @@ -169,13 +144,6 @@ void DFPT_Q0::build_stars(int nk) { stars_.assign(nk, std::vector(1, StarMember())); return; } - if (stardbg) { - std::cout << "STARDBG ik=" << ik << " j=" << j << " amap="; - for (int iat2 = 0; iat2 < nat; ++iat2) { - std::cout << mem.atom_map[iat2] << (iat2 + 1 < nat ? "," : ""); - } - std::cout << std::endl; - } stars_[ik].push_back(mem); } } @@ -341,26 +309,6 @@ void DFPT_Q0::pos_matrix(const psi::Psi>& psi, } } } - if (getenv("DFPT_Q0DBG") != nullptr) { - std::cout << "Q0DBG ik=" << ik << " tpiba=" << tpiba - << " npwk=" << npwk << std::endl; - 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); - double p2 = 0.0; - for (int d = 0; d < 3; ++d) { - p2 += std::norm(p_mat[m][n][d]); - } - std::cout << "Q0DBG p m=" << m << " n=" << n - << " de=" << de << " px=" << p_mat[m][n][0] - << " py=" << p_mat[m][n][1] - << " pz=" << p_mat[m][n][2] << std::endl; - } - } - } } } From 3152fff46567f0de6a474f7c4f9fade7780222ad Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Fri, 28 Aug 2026 18:08:32 +0800 Subject: [PATCH 39/50] delete PLAN --- .../module_dfpt/PLAN_dfpt_implementation.md | 599 ------------------ .../PLAN_reciprocal_grid_refactor.md | 116 ---- source/source_pw/module_dfpt/README.md | 116 ---- 3 files changed, 831 deletions(-) delete mode 100644 source/source_pw/module_dfpt/PLAN_dfpt_implementation.md delete mode 100644 source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md delete mode 100644 source/source_pw/module_dfpt/README.md diff --git a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md b/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md deleted file mode 100644 index 24b9c1a48fd..00000000000 --- a/source/source_pw/module_dfpt/PLAN_dfpt_implementation.md +++ /dev/null @@ -1,599 +0,0 @@ -# ABACUS DFPT 完整实施计划 - -> 本文件记录 DFPT(密度泛函微扰理论)落地计划。执行状态见末尾"进度"章节。 - -## 总原则 - -- 顺序:**U0(DFT+U 预留)→ C 物理主体(C0–C7)→ B 数据层收编 → A irrep 分解**。C4(Metal)仅留接口。 -- 每个子任务交付 = **代码 + 单元测试 + 物理对照**;阶段结束先 git 提交再继续。 -- 生产代码零新增 `GlobalV`/`PARAM` 依赖(module_dfpt 不读 PARAM,决策由 esolver 接线层做);C++11、LF、新文件进 CMakeLists。 -- 验证体系:金刚石 `stru_lib[0]`(O_h,a=1,1 个 C 原子,Γ 网格)为主;沙箱 OpenMPI 不作为回归基准。 -- 构建:`cmake --build build --target -j8`;ctest 回归过滤:`"MODULE_CELL_klist_test$|MODULE_CELL_reciprocal_grid_test|MODULE_CELL_qlist_test|MODULE_CELL_little_group_test|MODULE_DFPT"`。 -- 治理:`python3 tools/03_code_analysis/agent_governance_check.py --base upstream/develop --head HEAD --format text`。 - ---- - -## U0 — DFT+U 接口预留(数据+管线+桩+测试) - -前提(已核实):PW DFT+U 存在且已接线(`esolver_ks_pw.cpp:203` iter_init_dftu_pw、`hamilt_pw.cpp:126` OnsiteProj 入链、`setup_pot.cpp:75` OnsiteProjector 初始化),**但依赖 LCAO 轨道文件**(纯 PW 无文件则 locale 未初始化、实际跑不了)。设计据此自洽 on/off。 - -1. **`dfpt_pw_data.h/.cpp`**: - - 头内前向声明 `class Plus_U;`(保头文件依赖最小)。 - - `init(..., int nat, const Plus_U* dftu)` 末位加参数;新增 `with_u()`(= 指针非空)、`u_active()`(= 非空 **且 `dftu_->is_locale_initialized()`**,覆盖无轨道文件退化)、`get_dftu()`、`set_docc/get_docc`(每 q complex 向量,惰性分配)。 - - 更新调用点:`dfpt_pw.cpp:71`、`dfpt_irrep_data_test.cpp:172`。 -2. **`dfpt_pw.h/.cpp`**:`init(ucell, psi, nelec, ecutwfc, const Plus_U* dftu)`;头内前向声明、Impl 存指针;新增 `get_with_u()/get_u_active()`;更新测试调用点(传 nullptr)+ README 示例 + esolver 注释行。 -3. **`dfpt_rho.h/.cpp`**:新增 `cal_docc(psi, wg, q_idx, data)` 桩(`if(!data.with_u()) return;`,注明 C3 实装点)。 -4. **`dfpt_pert.h/.cpp`**:`build_dv` 末尾 `if(data.with_u()) build_dv_u(...)`;私有桩 `build_dv_u` 空实现(注明 C1 实装 frozen 项)。 -5. **`dfpt_phon.h/.cpp`**:新增 `dftu_onsite(q_idx, data)` 桩,`assemble` 中 when with_u 调用(C5 实装)。 -6. **`dfpt_q0.h/.cpp`**:仅加非局域 `[r,V_U]` commutator 预留注释(C6 延后)。 -7. **测试**:`dfpt_irrep_data_test` 更新 init + docc roundtrip + with_u=false 安全路径;`dfpt_pw_run_test` SOURCES 加 `../../../source_lcao/module_dftu/dftu.cpp`,新增 `Plus_U dftu;` 传非空 → `u_active()` 为 false(locale 未初始化)、`run()` 不崩、桩 safe(覆盖"无轨道文件"退化路径)。 -8. 验证:构建 2 测试目标 + 6 目标回归 + 治理。提交。 - -**DFT+U 物理特殊处理清单(后续实装点)**: -- 一阶占据矩阵 docc:交叉项 `becp(k+q,dψ)·becp(k,ψ)`(C3,依赖 dψ)+ 冻结项 `becp(k,ψ)·dbecp_f(k,ψ)`(GS k,复用 `cal_dbecp_f`)。 -- 一阶 U 势 dV_U(C1 frozen 实装):占据响应 `|φ(k+q)⟩U(diag·δ−docc)⟨φ(k)|ψ⟩`(需 SCF 自洽)+ 冻结 `|∂φ(k+q)/∂τ⟩V_eff⟨φ(k)|ψ⟩` 等(`Onsite_Proj_tools` 用 DFPT 的 k+q 基初始化即复用,相因子自动正确)。 -- Stern 的 H(k+q) 含零级 V_U:复用含 OnsiteProj 的 ops 链即自动覆盖。 -- 动力学矩阵 U 项(C5 `dftu_onsite`)、Q0 非局域项(C6)。 -- 治理:DFPT 内层 SCF 绝不调 `cal_occ_pw`(防覆盖 GS locale);零级 V_U 经 `get_eff_pot_pw_spin` 只读借用。 - ---- - -## C 阶段物理主体 - -**C0 — k+q 平面波基枚举** -- k+q 基枚举 helper:复用 `pw_basis_k.h` 的 `npwk/ig2ixyz_k/getgpluskcar`;生成每 (ik,q) 的 k+q 波矢 G 列表。 -- 单元测试(核对 G 集合与 Gamma 平移关系)。构建+测试后提交。 - -**C1 — DFPT_Pert 扰动构建** -- `dVloc_dtau`/`dVnl_dtau` 真实实现(q 相因子、USPP 投影子导数)。 -- `build_dv(q_idx, atom_idx, dir, data)` 组装 dV;`apply_dv`;`build_efield`。 -- **U0 实装点**:`build_dv_u` frozen 项(OnsiteProjector 已初始化时启用)。 -- 测试:dV 数值核对。提交。 - -**C2 — DFPT_Stern Sternheimer 求解** -- 移位哈密顿作用经 `LinearOperator` 抽象注入(dimension/apply):生产适配器复用 `ops->hPsi(hpsi_info)`(hsolver_pw.cpp:268-274 模式,C7 接线),单测注入解析算子。 -- 无状态 `solve(aop, occ_kq, b, max_iter, conv_thr, dpsi, residual)`:投影 CG(初始 x=0、r=P_c b,每步搜索方向重投影,收敛判据 `||P_c r||/||P_c b||`);`apply_pv` 双扫 MGS 投影(alias-safe);方程 `(H(k+q)−ε_n)P_c|dψ_n⟩=−P_c dV|ψ_n⟩`。 -- 测试:对角算子闭式解 + 稠密 Hermitian(U D U† 谱展开参考,eps 落占据带内验证投影)+ 正交性保持 + 退化 RHS。提交。 - -**C3 — DFPT_Rho 密度响应** -- `compute_drho`:交叉核 `A(r)=Σ_{kn occ} wg·u*·du`(K/rho 两条 FFT 路径同网格点乘,均无 k 相位 → Bloch 相位合并为单个 e^{iq·r});`drho_g` 为 rho 网格 q 移位系数 `A_Δ=Σ wg Σ_G c*_G d_{G+Δ}`;Δ=−q 落在倒格矢时投影为零(电荷守恒,q=Γ 必触发);`drho_r=2Re[e^{iqr}A]` 由投影后系数重建。USPP 增广项与 nspin>1 留 WARNING_QUIT 守卫(设计期)。 -- `mix_drho` 直接复用 `Base_Mixing::Plain_Mixing::plain_mix`(q 移位复空间混合 + 残差 `||out−in||/||out||`,首步 in=0),不套 `Charge::rho`/`Charge_Mixing`(头依赖由 charge_mixing.h 降为前向声明 + matrix3.h 值成员)。 -- **U0 实装点**:`cal_docc` 交叉项需 k/k+q 双端 β 投影子(PW 侧 vkb 适配器),与 Plus_U 生产接线同落 C7/U1 窗口;纯 PW 路径 `u_active()` 恒 false,安全退化保持。 -- 测试:G 空间暴力双和对照 + 实空间直接求和对照 + Γ 电荷守恒(ig0 置零 + 网格和≈0)+ 混合两步组合与残差公式。提交。 - -**C4 — DFPT_Metal(仅接口)** -- 本期不实现:`compute_drho`/occupation 响应留接口与设计说明(`is_metal_`/`dmu_` 数据已备)。 - -**C5 — DFPT_Phon 动力学矩阵** -- `assemble`:`ion_ion(q,dynmat)` + `electron(q_idx,data,dynmat)` 真实现(Ewald α 选取复用 `force_pw.cpp:479 cal_force_ew` 惯例:1.1 起步递降、upperbound<1e-6、跳 `ig_gge0`;仓库中 `H_Ewald_pw::rgen` 已不存在,以 `cal_force_ew` 为蓝本;相因子经 `symm.gtrans[48]`+`kgmatrix[48]`)。 -- `electron` 拆两步:`accumulate_electron(q,atom,dir,psi,wg,data)`(2n+1 形式 `D_ab=(2/Nk)Σ_kn wg·⟨dψ^b|dV^a_ext|ψ⟩` 复数逐 k 累积,虚部经 k-star 配对共轭相消/assemble Hermitian 对称化吸收;run() 每方向 SCF 收敛后即时调用,dpsi 存储不加方向维度,避免 B 前翻搅数据层)+ `assemble` 合并 ion_ion + 累积电子项。 -- **U0 实装点**:`dftu_onsite`/`dftu_lambda` 电子项。 -- `diagonalize` 真实现(`LapackConnector::zheev`,`ω=sign(e)√|e|` 换算 cm⁻¹,质量因子 1/√(MM′));`add_loto` 非解析项 `(4π/Ω)(q·Z*_a)(q·Z*_b)/(q·ε∞·q)/√(MM′)`;`check_sum_rule` Γ 声学 3 零模 + 列和。 -- 测试:ion_ion vs 小胞朴素双和;Γ ASR;accumulate_electron vs 注入 dpsi 闭式收缩;zheev vs 已知矩阵;loto 各向同性解析极限。提交。 - -**C6 — DFPT_Q0 介电/Born/LO-TO** -- 新增 `v_hartree_q`:`dV_H(G)=4π|G+q|²⁻¹·drho_g`,跳过 |G+q|=0(ig=−q),约定对齐 `source_estate/module_pot/h_hartree_pw.cpp:16`;实现为 `DFPT_Rho` 成员,**同时服务 C7 全 q 点 SCF 屏蔽势**。 -- **XC 一阶核(回调注入复用 `PotXC_FDM`)**:库里已有 `elecstate::PotXC_FDM`(`source_estate/module_pot/pot_xc_fdm.cpp:39`,`δV_xc=V_xc[ρ₀+δρ]−V_xc[ρ₀]`,LCAO 侧 `veff_dh.cpp:417 cal_dH_hf_xc` 已验证同构物理)。module_dfpt 内只定义回调契约 `XC_First_Order`(抽象类,镜像 `DFPT_Stern::LinearOperator` 惯例),esolver 接线层写 `PotXC_FDM_Adapter` 注入;module_dfpt 不 include `pot_xc_fdm.h`(头依赖最小)。复 δρ 拆 Re/Im 两次调用再重组(线性叠加合法,误差 O(δρ²))。`LR::KernelXC`(LIBXC 解析核,module_lr)列为后续优化,本轮不动。 -- `pos_matrix`:不走病态位置算符,用 `[Ĥ_SCF,r]` 速度算符等价式 `⟨u_m|r|u_n⟩=(ε_m−ε_n)⁻¹⟨u_m|[V_nl,r]|u_n⟩`(m≠n);`[V_nl,r]` 复用 C1 `build_vkb` 平移基列表求导。 -- 非局域 `[r,V_U]` commutator 项记录为 U 预留。 -- 测试:v_hartree_q 单 G 闭式;XC 回调 vs 解析 LDA 核 `(4/9)v_xc/ρ`;ε/Z* 金刚石对称性约束;loto 方向极限。提交。 - -**C7 — run() 接线 + ESolver/INPUT** -- `DFPT_PW::init` 扩签名:`(..., pw_rho, pw_wfc, sf, wg, eig, const XC_First_Order* xc)`(规则 5:不加默认参,全调用点更新);`nrxx=pw_rho->nrxx`;`pert_/rho_/q0_/phon_` 真 init。 -- Stern 生产适配器 `HamiltShiftAdapter : DFPT_Stern::LinearOperator` 包 `p_hamilt->ops->hPsi`(`hsolver_pw.cpp:271-273` hpsi_info 模式);占据态 `occ_kq` 由 GS ψ 经 k+q 基投影(复用 `apply_pv` MGS)。 -- `run()` 实装:mode basis 为空(A 前置占位)→ 回退遍历 3N 方向;SCF 内环 `dv_sc=dv_ext+v_hartree_q+xc_->apply(drho_in)` → `stern_.solve` → `compute_drho` → `mix_drho`,残差=4*ecutwfc` 写入文档)。 -- `esolver_dfpt_pw.cpp`:解开 `init` 注释,实参 `*this->stp.psi_cpu` + `PARAM.inp.nelec` + `PARAM.inp.ecutwfc` + `this->pw_wfc`/`this->pw_rho`/`this->sf` + `dft_plus_u ? &this->dftu : nullptr`(`esolver_ks.h:63`)+ PotXC_FDM 适配器(持 GS `Charge`,复 δρ 拆 Re/Im);内层 SCF 禁调 `cal_occ_pw`(U0 治理条目);`esolver.cpp` 工厂加 `"dfpt"` 分支。 -- INPUT 行为若变则同步 `docs/parameters.yaml` + `input-main.md`;验证 `./build/abacus -h esolver_type` 与 `--check-input`。 -- 金刚石端到端对照:声学 3 零模(ASR)+ 光学支 LDA 文献区间 + 介电/Born;`./build/abacus --version` 记录身份。 -- 全量构建 + 回归 + 治理。提交。 - ---- - -## B — 工程化收编(2026-08-18 修订:先全流程工程验证,对称性后移) - -> 决策记录:INPUT 主文件参数(非 dfpt.in 子文件);irrep 保留接口、优先全流程工程验证; -> 调试插桩暂缓清理(A 阶段验证后统一收尾)。执行序:B1 → B0 → B2 → B3 → B4,每节点 -> 完成 = 代码 + 构建/回归/治理 + git 提交 + 本文档进度回写,再进入下一节点。 - -**B1 — INPUT 参数接线(解除硬编码/死代码)** -- 新增 dfpt 前缀 INPUT 参数:`dfpt_qmesh`(3 int) / `dfpt_qfile`(str,走 `QList::read_from_file`) - / `dfpt_compute_q0`(bool) / `dfpt_loto`(bool) / `dfpt_conv_thr` / `dfpt_max_iter` / `dfpt_mix_beta`。 -- `esolver_dfpt_pw.cpp` 删除硬编码(set_qmesh(1,1,1)/conv_thr/max_iter/空桩 set_parameters), - 改从 `inp` 显式传递(规则 1);`set_compute_q0`/`set_loto` 死代码开关经此激活。 -- `docs/parameters.yaml` + `input-main.md` 同步;`-h dfpt_*` 与 `--check-input` 验证。 - -**B0 — 全流程工程验证(数值验收基线)** -- 真实金刚石 Γ 点(a≈3.567 Å、NC PP、收敛 k 网格;玩具胞 742 cm⁻¹ 仅为 FD 锁定基线): - 声子(ASR + 光学支 vs LDA 文献)、ε∞(各向同性 ≈5.3–5.7)、Z*、LO-TO 方向依赖。 -- 非 Γ q:`dfpt_qmesh` + 公度 k 网格(`build_occ_kq` 公度守卫已备)验证 q≠0 全流程。 -- MPI>1 rank 冒烟(当前打印注释自述 single-rank,未验证面)。 -- `--version`/`-h`/`--check-input` 记录;结论回写本文档。 - -**B2 — ε∞/Z*/LO-TO 输出正式化** -- esolver design-phase std::cout 转正式输出:多 q 布局、LO-TO 修正后频率(每方向)。 -- loto 方向经数据层传递,消除 run() 中 (1,1,1)/√3 硬编码。 - -**B3 — Kerker 型预条件混合** -- `DFPT_Rho` 内自实现 `|G+q|²/(|G+q|²+a²)` 预条件(不引 charge_mixing.h,module_base 无 - 现成 Kerker 已核实);mix_type 支持 plain/kerker。 -- 验收:λ_A1≈−2.2 模型问题 β=0.7 收敛(β 上界 0.62 解除,JPROBE 复用为验收工具); - 金刚石频率与 β 无关(固定点正确性);`dfpt_mix_beta` 默认回调并文档记录。 - -**B4 — 数据层收编** -- 收敛台账(converged_/residuals_/current_iter_ 按 (q,irrep))并入 `DFPT_PW_Data`; - 删除 `DFPT_IrrepData` 适配层与 `get_dpsi_obj` static dummy;测试迁移; - **保留 (q,irrep) 接口形状**(为 irrep 实装留插槽);run() 外层 while 记账语义梳理。 - -**暂缓项(接口保留,后续单独立项)** -- A irrep 分解:LittleGroup 占位(nirr≡1/空 basis)与 run() 3N 回退保持现状。 -- 插桩清理(PTCHK/DYNCHK/MDBG/JPROBE/VKBCHK/DFPT_MIX_BETA):B3 验收仍需 JPROBE。 -- KVectorUtils 薄封装删除:随 A 阶段收尾一并处理。 - -## A — irrep 分解(最后,工程验证完成后立项) - -- `module_symmetry/little_group.{h,cpp}`:完整不可约表示表 + 投影算子 → 真实 `get_nirr`/`get_mode_basis`(替换占位 =1/空)。 -- 测试:金刚石/闪锌矿 Γ/X/L 点 irrep 分解与理论表核对。提交。 -- 收尾:LO-TO 一般方向经 irrep 机制;KVectorUtils 薄封装删除(reciprocal_grid - 重构遗留,随本阶段一并处理)。 - ---- - -## 附注:重构计划(PLAN_reciprocal_grid_refactor.md)状态交叉核对(2026-08-19) - -- Phase 1–4 实质完成:ReciprocalGrid 基类 + K_Vectors/QList 继承 + - LittleGroup 接口占位(nirr≡1)+ DFPT INPUT 驱动接线。 -- 遗留:KVectorUtils 薄封装删除 → 随 A 阶段收尾;LittleGroup 完整 irrep 表 - → 即本计划 A 阶段。 -- 环境注记:当前 build 树未注册 CELL 侧 klist/reciprocal_grid/qlist/ - little_group 测试(需重新 cmake configure 才能跑全量回归)。 - ---- - -## 风险与注意事项 - -- `Plus_U dftu` 是对象成员,指针永远非空 → `with_u` 由 esolver 在 `dft_plus_u` 时传非空指针决定,语义干净。 -- 无轨道文件 → `u_active()` 为 false 安全退化;测试显式覆盖该路径。 -- `dftu.cpp` 加入测试链接依赖 LCAO=OFF 配置;若 CI 在 LCAO=ON 下编译该测试需补 hamilt 依赖(记录在案)。 -- 沙箱 OpenMPI 警告(`opal_ifinit`)为环境产物,不作为失败判据。 - -## 进度 - -- [x] 计划定稿(U0/C/B/A 序列、DFT+U 依赖轨道文件的 on/off 自洽设计) -- [x] U0 DFT+U 接口预留 `3599973fa` - - `Plus_U*` 经 `DFPT_PW::init`/`DFPT_PW_Data` 线程化(esolver 接线层决策,module_dfpt 不读全局输入) - - `with_u()`/`u_active()`(locale 未初始化即无轨道文件 → 安全退化)+ 每 q `docc_` 槽位 - - 桩:`DFPT_Rho::cal_docc`、`DFPT_Pert::build_dv_u`、`DFPT_Phon::dftu_onsite`、Q0 `[r,V_U]` 注释 - - 测试 5+3 全过;`dftu_test_support.cpp` shim(免 LCAO 侧链接闭包);6 目标回归 + `abacus_pw_para` 链接通过 -- [x] C0 k+q 平面波基枚举 - - `DFPT_KQ_Basis`:复用 GS 复杂 k 基的共享 G 网格,仅做 k+q 平移中心再过滤(`|G+k+q|^2<=gk_ecut`),无需新建 FFT 网格/重分发;前置条件 gamma_only=false + 网格截断覆盖 k+q 球(`gridecut_lat >= (sqrt(gk_ecut)+max|k|+max|q|)^2`,ecutrho>=4*ecutwfc 满足) - - `get_npwk/get_ig/get_ig2isz/get_gcar/get_gpluskq/get_gk2/get_kplusq` 访问器;gamma_only 守卫 WARNING_QUIT - - 测试 5 项全过(Γ q=0 全等复现、偏心非对称球、k+q 平移不变性、非零 q 与全网格穷举对照、null/gamma_only 拒绝);7 目标回归 -- [x] C1 DFPT_Pert - - `dVloc_dtau`:rho 网格系数 `i·tpiba·(Δ+q)_dir·Vloc(|Δ+q|)·e^{i2π(Δ+q)·τ}`(Δ+q=0 分量剔除);`vloc_at_g` Coulomb 解析式(复刻 `vl_pw::vloc_coulomb`)+ numeric 径向 FT(复刻 `vloc_of_g` 含 erf 补偿) - - `dVnl_dtau`:NC 分离算符两项恒等式 `i·tpiba·(k+q+G'')_dir·(Vnl|ψ⟩ − Vnl[i·tpiba·(k+G')_dir|ψ⟩`;`build_vkb`((−i)^l·Y_lm·(4π/√Ω)∫β j_l r dr·e^{i2π·gk·τ},GS 约定对齐)+ `radial_vq`(Simpson)+ `real_ylm`(l≤2);USPP/SOC WARNING_QUIT 守卫 - - `build_dv`→`set_dv_recip_c`→recip2real→`set_dv_rc`;`apply_dv`(纯循环卷积,q 相位已并入系数)+ `build_efield`(−E·r)+ `build_dv_u`(u_active 守卫,C7 激活) - - 串行测试目录 `test_serial/`(`__MPI` 整体关闭 + `dfpt_planewave_serial` OBJECT 库,ABI 一致)8 项全过:rho_gvec≡gcar、dVloc 有限差分(含 q≠0/双方向)、apply_dv 卷积 vs 解析矩阵元、efield 斜坡闭式 FT、build_vkb 独立 Simpson+τ 纯相位、dVnl 两项恒等式 vs 算符有限差分、USPP 拒绝、with_u/u_active 安全退化 - - 测试捕获并修复 3 处约定/实现错误:① 相位幅角 `tpiba·(w·τ)` → `TWO_PI·(w·τ)`(GS `stru_fac` 的 e^{i2π(g·τ)} 约定,tau 为 lat0 单位);② 实空间布局 `ir=(ix·ny+iy)·nz+iz`(z 最快,冲击响应探针钉死;build_efield 原假设反向);③ rho/wfc 棒表枚举不同 G 球 → isz 编码不可互换,`real_space_dv` 改经 FFT 胞 (ix,iy,iz) 三元组反查 - - 已知边界(C7 处理):单 k 基时 k+q 球需 wfc G 列表含 `sqrt(gk_ecut)+|k+q|` 半径(k 网格覆盖或 inflate);并行 pool 实空间布局 - - 8 目标回归全过(CELL 4 + DFPT 4);`abacus_pw_para` 链接通过 -- [x] C2 DFPT_Stern - - 无状态投影 CG:`DFPT_Stern::solve`(x=0 起步,α=|r|²/(pᵀAp)、β=|r_new|²/|r_old|²,搜索方向每步 `P_c` 重投影;pAp≤0 时残差方向重启);`apply_pv` 双扫 MGS(alias-safe);收敛 `||P_c r||/||P_c b||`;末步解 hygiene 投影 - - `LinearOperator` 注入(dimension/apply):生产 hPsi 适配器留 C7;金属/dmu 分支留 C4 - - 边界行为:b 全在占据子空间 / b=0 / 维数不匹配 → dpsi=0、residual=0、返回 0 次迭代 - - 测试 5 项全过(MPI 侧 `MODULE_DFPT_stern_test`):对角算子 vs 闭式补空间解、稠密 Hermitian(Givens+相位酉 U,eps=1.7 落占据带内)vs 谱展开参考、解对随机占据集正交性 <1e-9、占据子空间退化 RHS、零 RHS - - 9 目标回归全过(CELL 4 + DFPT 5);`abacus_pw_para` 链接通过;治理仅既有两类豁免 WARNING(头文件值类型 include、设计期模块 docs-sync) -- [x] C3 DFPT_Rho - - `compute_drho`:每 (q,k) 经 `DFPT_KQ_Basis` 重建 k+q 基,dpsi 系数经 (ix,iy,iz) 反查散布到 rho 网格(C1 模式);`u`=K 基 recip2real、`du`=rho 网格 recip2real,同网格共轭积累加 `A(r)`;real2recip → `drho_g`(q 移位系数);Δ=−q(Miller 逆解 + 舍入判定)投影零;`drho_r` 从投影后系数重建(双存储一致);占据门 `wg<1e-8` 跳过 - - `mix_drho`:`Plain_Mixing::plain_mix` 复空间混合(首步 in=0 → mixed=β·out,残差=1),混合后重建 `drho_r`;`init` 增加 `recip_matrix`(G 矩阵,q_frac→cart),非 plain 混合 WARNING_QUIT;nspin≠1 WARNING_QUIT(自旋- k 排序未钉死,C7 定) - - 数据层 `set/get_drho_r/g` 由桩转正式存储;irrep 包装测试同步翻转(round-trip 非空) - - 测试捕获并修复测试侧 2 处参考错误(生产代码无 bug):① `PW_Basis_K::gcar` 是逐 k 数组(`ik*npwk_max+igl`,pw_basis_k.cpp:261-286),按基球 ig 读是错的——参考列表改按 igl 直读;② 直接求和参考混用 cart G 与 frac r(相位差 lat0 倍)——改 `r_cart=frac·latvec` 后 `g·r_cart` - - 串行测试 5 项全过(`MODULE_DFPT_rho_serial`):G 空间 vs 暴力双和(<1e-10)、实空间 vs 直接求和(5 采样点 <1e-9)、Γ 电荷守恒(ig0 置零 + Σdrho_r/|max|/N <1e-12)、混合首步=β·out 且残差=1、第二步组合公式 + 残差 - - 10 目标回归全过(CELL 4 + DFPT 6);`abacus_pw_para` 链接通过;治理仅既有豁免 WARNING(头文件净减 charge_mixing.h) -- [x] C4 DFPT_Metal(仅接口) - - `dfdeps`/`compute_dmu`/`compute_drho_metal` 加 WARNING_QUIT 守卫("not supported in the design phase"),设计期金属分支显式拒绝而非静默错值;`sigma_`/`smearing_type_` 与数据层 `is_metal_`/`dmu_` 槽位保留 -- [x] C5 DFPT_Phon - - `ion_ion`:G 空间(Poisson 对偶恒等式,`w=G+q` 核 `w_a w_b/w²·e^{-w²/4α}`)+ 实空间(erfc Hessian 双循环,`r_c=6/√α`)+ 自项相位差;对角元 phase-free 交叉原子累积(`-√(Mb/Ma)` 系数)+ 自镜像 `(e^{i2πq·L}−1)` 项;α 选取复用 `cal_force_ew` 惯例(1.1×0.9^n,upperbound<1e-6);Γ ASR 由构造精确成立 - - `accumulate_electron`:2n+1 复数累积 `2Σwg⟨dψ^b|dV^a_ext|ψ⟩`(cross 项经 `apply_dv` 复用 C1 全部约定)+ 同原子非谐项 `Σwg⟨ψ|d²V_loc+d²V_nl|ψ⟩`(`d2vloc_r` rho 网格核 + `apply_d2vnl` 四项 β 恒等式,均已在 C1/C5 实现并测试);dpsi 槽备份/恢复(apply_dv 复用槽位) - - `diagonalize`:`LapackConnector::zheev`,`ω=sgn(e)√|e|` 换算 cm⁻¹(独立 CODATA 常数交叉验证);`add_loto` `(4πe²/Ω)(q̂Z*_a)(q̂Z*_b)/(q̂ε∞q̂)/√(MM′)`;`check_sum_rule` Γ 行和 - - 测试捕获并修复 2 处错误:① 生产 cross 项 `dot.real()` 丢虚部——q≠0 时单 k 矩阵元复数(虚部 k-star 配对相消),assemble Hermitian 对称化依赖复数项,改复数累积;② 测试期望动量缺 `+q`(用 `gpluskq` 直接当动量)——dV 系数动量是 `Δ+q`(与 C1 pert 测试 `AnalyticDVloc(gpp+q_cart)` 一致),手算数值双向定位后修正为 `w=g+q_cart`,d2 期望同步补 `wg` 占据因子 - - 串行测试 `MODULE_DFPT_phon_serial` 7 项全过:Γ ASR(双原子破对称胞)、Γ 声学 3 零模、非公度 q vs 朴素偶极 Hessian 双和、accumulate_electron vs 注入 dpsi 闭式收缩、zheev vs 已知矩阵、loto 各向同性解析极限、Γ 求和规则 - - 11 目标回归全过(CELL 4 + DFPT 7);`abacus_pw_para` 链接通过;治理仅既有两类豁免 WARNING -- [x] C6 DFPT_Q0 - - `v_hartree_q`(DFPT_Rho 成员):`dV_H(G)=e²·4π/(tpiba²·|G+q|²)·drho_g`(w=gcar+q_cart,1/lat0 单位),跳过 |G+q|=0(对齐 `h_hartree_pw.cpp` 跳 ig_gge0 惯例);同函数服务 C7 全 q 屏蔽势 - - `XC_First_Order` 抽象契约(`apply(drho_r, dvxc_r)`,module_dfpt 不 include pot_xc_fdm.h,镜像 Stern::LinearOperator 注入惯例);PotXC_FDM 适配器(复 δρ 拆 Re/Im)落 C7 esolver 层,XC 核数值对照随 C7 适配器一并测试 - - `build_vkb_dk`(C1 build_vkb 的 k 导数,转 public 供 Q0 复用):三链解析导数——原子相位 `i2πτ_dir`、径向 `vq'(g)·tpiba·ghat_dir`(radial_vq 中心差分 dg=1e-4)、实谐函数方向链 `(e_dir−ghat·ghat_dir)/|G|`(l≤2 `grad_real_ylm`);G=0 处 l≥1 行方向链奇异(测度为零,仅相位项,与 QE 同处理) - - `pos_matrix` 速度算符形式:`⟨u_m|r_d|u_n⟩=−i·⟨u_m|dH/dk_d|u_n⟩/(tpiba·(ε_m−ε_n))`([H,r]=−i·dH/dk,k 取 2π/lat0 无量纲导数与 build_vkb_dk 一致,r 出 bohr);dH/dk = 动能 `2tpiba²(k+G)_d` + 非局域 `|dvkb⟩D⟨vkb|+|vkb⟩D⟨dvkb|`(D=dion·m 选择规则,dVnl_dtau 布局);V_loc 与 k 无关;严格简并对跳过(规范依赖) - - `compute_eps`:`ε_ab=δ_ab+(8π/Ω)Σ_k wg Re[r_a r_b]/(ε_c−ε_v)/Nk`(长度规范分母,与振子强度和规则一致;绝对值标定 C7 金刚石端到端);`compute_born`:`Z*_{k,ab}=Z_k δ_ab−(4/Nk)Σ wg Re[⟨v|dV_b|m⟩⟨m|r_a|v⟩]/(ε_m−ε_v)`(m 跑全部带含占据;`⟨v|dV|m⟩=conj(dv_mv)`;经 C1 apply_dv@q=0 复用全部约定,dpsi 槽备份/恢复仿 phon 模式;离子 Z 只加 (a==b) 对角,每原子单次 set_born) - - 串行测试 `MODULE_DFPT_q0_serial` 5 项全过:build_vkb_dk vs build_vkb 中心差分(泛型 gk 列表,1e-5)、pos_matrix 动能项闭式(−i 因子/tpiba 标定/Hermitian/简对跳过)、非局域收缩 vs 算符有限差分(ψ(G=0) 列置零避开奇点)、compute_eps 二能级全系数链(复激发态敏感于 conj 位置)、compute_born vs 闭式 G 求和(含离子对角+dpsi 恢复) - - `MODULE_DFPT_rho_serial` 增 v_hartree_q 3 检查(单 G 闭式、|G+q|=0 跳过、尺寸守卫清空),6 项全过 - - 12 目标回归全过(CELL 4 + DFPT 8);`abacus_pw_para` 链接通过;治理仅既有豁免 WARNING(docs-sync) -- [x] C7 run() 接线 + ESolver/INPUT(模块层 + esolver 工厂接线完成;金刚石端到端数值对照随 B 前置验证补做) - - 模块层(C7a): - - `DFPT_PW::init` 新签名 `(ucell, psi, pw_rho, pw_wfc, sf, veff_r, wg, eig, xc, nelec, ecutwfc, dftu)`(规则 5:不加默认参,全调用点更新,含 pw_run_test 骨架模式传空基);Impl 持 GS 基/veff/wg/eig + `DFPT_HamiltShift* hamilt_` + `occ_kq_` 缓存 - - `DFPT_HamiltShift : DFPT_Stern::LinearOperator`(新文件 `dfpt_hamilt_shift.{h,cpp}`):H(k+q) 不复用 GS HamiltPW 链(ik 索引绑定 gk2/vkb,不可平移)→ 自组装三部分——动能 `tpiba²·kq.get_gk2(igl)` 对角 + veff_r FFT 卷积(kq2rho_ 经 FFT-cell triple 映射,C1 惯例)+ 缓存 k+q vkb 的分离非局域(dion m 选择规则同 dVnl_dtau 布局);`set_context(q_idx,k_idx)` 缓存投影 / `set_shift(eps)` 每 solve 更新对角 - - `DFPT_Pert::apply_vr`(public):屏蔽响应势作用全带(v_sc_r 与 dv_rc 同约定:q 移位复周期振幅);`real_space_dv` 重构为委托私有 `apply_vr_core`(FFT-cell triple 散射/收集核心共用);`build_vkb/build_vkb_dk` 保持 public(Q0 复用) - - `DFPT_Rho::reset_mixing(q_idx)`:清 drho_in_/residual_,每位移重开 SCF - - `build_occ_kq(q_idx)`:k+q 折叠匹配 GS k 列表(`kq ≡ k' (mod G)` 容差 1e-8;不匹配 WARNING_QUIT,需 Monkhorst 网格);占据态经共享 FFT-cell triple 从 ikq 的 G 球映射到 k+q 列表 - - `solve_displacement(q_idx,iat,idir)` 完整位移级 SCF 内环:`v_hartree_q(drho_g) + xc_->apply(drho_r)` 组 v_sc_r → `apply_dv + apply_vr` 组 RHS → `set_shift + stern_.solve` 每占据带 → `compute_drho + mix_drho` 残差收敛判据 - - `run()`:q=0 时 q0 响应(eps/Born/loto);每 irrep 位移循环 + `accumulate_electron`;`assemble + diagonalize + add_loto`(loto 方向默认 (1,1,1)/√3,一般方向随 A 阶段 irrep 机制);null 基保持骨架首迭代收敛退化(测试兼容) - - esolver 层(C7b): - - `esolver_dfpt_pw.{h,cpp}` 重写:`before_all_runners` 只做静态配置 + 从 `inp` 捕获 nspin/nelec/ecutwfc/dft_plus_u(规则 1:显式传递,init_dfpt 不读全局记录);`runner` 先 `run_gs`(复用 `ESolver_KS_PW::runner`)→ `init_dfpt` 真接线(GS 收敛后 veff/charge/psi 才存在)→ `dfpt_->run()` - - `init_dfpt` 实参:`*this->stp.psi_cpu`、`this->pw_rho/pw_wfc`、`&this->sf`、`get_veff_smooth()` 行 0 展开(`update_from_charge` 每迭代调 `interpolate_vrs`,收敛后即当前值)、`pelec->wg/ekb`、`XC_First_Order_FDM` 适配器(Re/Im 拆分过 `PotXC_FDM` 有限差分核,线性重组精确到 O(|δρ|²);持 GS Charge + scratch Charge)、`dft_plus_u ? &this->dftu : nullptr`;守卫:nspin≠1 / charge 不在 rho 网格(USPP)/ veff_smooth 网格不匹配 → WARNING_QUIT - - `esolver.cpp` 工厂:`determine_type` pw 分支加 `"dfpt"→"dfpt_pw"` + `init_esolver` 分支(治理豁免:determine_type 既有 PARAM 读取惯例,1 行) - - `read_inp_sys.cpp`:esolver_types 合法值加 `"dfpt"` + 注释/description 更新;`docs/parameters.yaml` + `docs/advanced/input_files/input-main.md` 同步 - - 验证:`cmake --build` esolver/abacus_pw_para/12 测试目标全绿;ctest 12/12(CELL 4 + DFPT 8);`abacus_pw_para -h esolver_type` 显示 dfpt 条目;`--version` v3.11.0-beta8;治理仅 determine_type 工厂 1 处豁免 ERROR + 既有 header/docs WARNING - - 待办(随 B/前置验证):金刚石端到端声子/ε∞/Z* 对照、`--check-input` 从有效算例目录验证 -- [x] 校准:屏蔽通道三处修复(金刚石 2 原子 smoke,24³ rho 网格,NC PP,Γ 点) - - FD/Ewald 锁定基线:e11=+0.08056、e12=−0.08059 Ry/bohr²(预质量 0.0028685/−0.0028701),光学 ~742 cm⁻¹;GS 力 FD 交叉验证 dV 装配(F_x 偏差 0.02%) - - 修复 1(dfpt_rho.cpp compute_drho):q=0 Hermitian 完成的 in-place `drho_g[ig] += conj(drho_g[gm])` 逐点双重处理 ±G 对(第二次访问读到已更新的第一项)→ 结果破坏 Hermitian 性,实空间重建混入 Re a(r) 寄生分量(均匀 ~1.25 过冲、对称违反被放大 1.3-1.8%、A1 投影 3.7%);最终实现 = 实空间 `2 Re a(r)` 预对称化后再 real2recip(实数组 FFT 本征 Hermitian,单边 stick(−G 不在球内)亦获正确完成值,替代 G 空间逐点镜像) - - 修复 2(esolver_dfpt_pw.cpp XC_First_Order_FDM):前向差分 `Vxc[ρ+δρ]−Vxc[ρ]` 的曲率项 ½Vxc″δρ²(T2⊗T2⊃A1)向 v_sc 泄漏寄生 A1(band0 ⟨dv_sc⟩=+0.0173 违反 A1⊗T2⊗A1 选择定则、占据三重态迹 +0.052)且二次非线性反馈使混合迭代 β=0.7 超指数暴走;改 η=1e-6 中心差分(Re/Im 各一对 cal_v_eff 探测)后泄漏 ~1e-11,默认 β=0.7 恢复收敛 - - 修复 3(dfpt_pw.cpp solve_displacement):`reset_mixing` 只清混合器内部态,data 层 `drho_g` 残留上一位移响应(含发散残渣)泄漏进新位移首迭代 v_sc;进入位移时同步清零 - - 修复后(默认 β=0.7,~76 s):光学 742.367×3(FD ~742)、声学 6.40×3(ASR:e11+e12=3.1e-6)、e11=0.00286804(目标 0.0028685)、e12=−0.00286494(目标 −0.0028701)、非 irrep 元 ~1e-11、收敛 drho 小群违反 0.000000/A1 投影 5e-6(对称性精确);裸响应(β=0.001 dump)小群违反 ~0.1% 确认裸链(Sternheimer/dV/dψ)干净 - - 后期漂移根因(本轮确诊):残差降至 5e-5 后指数增长(1.27×/iter)、|in| 恒定而 out 偏离 → 垃圾方向与物理分量正交、混合映射本征值 μ=1.2765 恒定(纯本征模);本征模身份 = {200} 壳 6 矢等幅实系数 + {111} 壳 8 矢 ±π/4 相位的 Hermitian 实 A1 呼吸模(seed ~1e-6 舍入级);均匀探针实验(DFPT_JPROBE:注入纯 A1 模 + rhs 去 dV_ext 单迭代直测线性映射)给出 λ_A1 = −2.229(Hartree-only −3.180,XC 削减到 −2.23)——非符号 bug,是最小 G 壳的 Coulomb 刚性(4π/G² 硬核):plain mixing 收敛条件 −2/β+1<λ 要求 β<0.62,物理 T2 模 λ=−1.42(小 G 头部含量少)在 β=0.7 恰好可收敛,故固定点正确而 A1 通道发散;β=0.4 时 μ_A1=−0.29 稳定 - - 修复:默认 mix_beta 0.7→0.4(注释记录测得的 |λ|~2.2 与 β 上界 2/(1+|λ_min|),留裕量至 |λ|~5);DFPT_MIX_BETA env 旋钮保留;β=0.4 时 6 位移全部经收敛旗标退出(平均 ~38 iter,总 228),频率/ele 矩阵与 β 无关逐位一致(固定点正确性再验证),收敛 drho manifest 干净(|FD| 比率 0.99994、cos 0.9993、逐点相对差 3.8%);后续正解是 Kerker 型预条件混合(随 B 阶段排期) - - ε∞/Z* 打印为空(随 B 阶段);调试插桩(PTCHK/DYNCHK/MDBG/JPROBE dump/VKBCHK/drho dump/DFPT_MIX_BETA env)收尾节点统一清理评审 -- [x] B 工程化收编(2026-08-18 修订:B1 INPUT 接线 → B0 全流程验证 → B2 输出正式化 → B3 Kerker → B4 数据层;2026-08-24 全部完成) - - 修订依据的差距盘点(代码 vs 计划交叉核对,2026-08-18): - ① `set_compute_q0`/`set_loto` 全仓库无调用者(死代码,q0/loto 分支不可达); - ② `set_parameters("dfpt.in")` 空桩 + esolver 硬编码 `set_qmesh(1,1,1)`/conv_thr/max_iter(非 Γ q 无法从输入驱动); - ③ ε∞/Z* 打印为 design-phase 临时 std::cout(esolver_dfpt_pw.cpp:322,单 rank 假定); - ④ 混合仍 plain β=0.4(Kerker 预条件排期 B3); - ⑤ DFPT_IrrepData irrep 维度占位穿透、get_dpsi_obj 返回 static dummy; - ⑥ run() 外层 while 形式化(无条件 set_converged(true)、LO-TO 方向硬编码 (1,1,1)/√3; - ⑦ 真实晶格金刚石端到端/非 Γ q/MPI>1 rank 均未冒烟(C7 待办未做)。 - - [x] B1 INPUT 参数接线 `297a2b3a2` - - `read_inp_dfpt.cpp` 7 项(qmesh/qfile/compute_q0/loto/conv_thr/max_iter/mix_beta, - check_value:loto 需 compute_q0、qmesh≥1、mix_beta∈(0,1]);CMake 两处接入 - - esolver 删硬编码与 `dfpt.in` 空桩,`set_parameters` 移除,全走显式 setter(规则 1); - `DFPT_PW` 新增 set_qfile/set_mix_beta/set_compute_q0/set_loto(q 文件优先于 MP 网格) - - `QList::read_from_file` 改填占位 A1 irrep(nirr=1)而非清空——q 文件路径下 run() - 依赖 get_nirr≥1 才进 3N 回退求解 - - 修复(测试捕获):空默认串参数回写 INPUT 再读回时 `str_values[0]` 越界 → dfpt_qfile - 采用 pseudo_dir 的 `get_size()==0` 守卫模式 - - docs/parameters.yaml 新类目 + input-main.md 经 docs/generate_input_main.py 再生; - `-h dfpt_qmesh/dfpt_mix_beta` 验证;回归 14/14(CELL 4 + DFPT 8 + IO 2); - 治理仅既有豁免 WARNING - - [ ] B0 全流程工程验证(真实金刚石 Γ / 非 Γ q / MPI 冒烟) - - [x] 多 k(nk>1)数值错误根因与修复 `(本轮,6 文件 +374/−57)` - - 排除法完成:sym=0/1、v_sc 假设、LAPACK/BLAS、npw 不匹配、球大小不匹配({L 392, X - 388} 完全正确)、δρ-cube FD 路线(k 采样噪声地板 >> 位移信号,判死)、d2/计算 - 通道(d2 混合=孤立预测精确一致) - - **根因 1(标签折叠)**:build_occ_kq 假设 k+q 球与 k(ikq) 球共享同 FFT 胞 G 标签; - {L,−L} 时 −L 折叠到 L 标签(差 b1),标签错位 → 投影态垃圾 → 核检查失败+发散。 - 修复 = 倒格矢整数三元组匹配 f+dn=f'(dn=k(ik)+q−k(ikq)),ikq 侧标签经 - `PW_Basis_K::getgcar` 读取——关键发现:`collect_local_pw(erf)` 把 gcar 重建为 - per-k 球布局 [ik*npwk_max+igl],父类全局 ig 布局已毁(nk=1 曾靠堆残留"幸运"通过) - - **根因 2(smearing 投影悬崖)**:wg<1e-8 绝对阈使投影器随 k 采样跳变——{Γ,L} 采样 - 的 E_f 使 L 带占据带尾 w=5e-6 跨过阈值,进入 P_c 投影 → 其空态通道在 (H−ε)⁻¹ - 中关闭 → 收敛响应差 ~10%(X03 +46%、ASR 行和违反 21%)。权重 1% Γ 实验证实损伤 - 与 w_Γ 无关(结构性);β=0 单迭代实验证实同 rhs/本征值下 |dψ| 差 8%(纯投影效应)。 - 修复 = 共享 `dfpt_band_occupied()`:wg(ik,ib) > 0.5·wg(ik,0)(多数占据判据), - 一致应用于投影器/求解驱动/drho/2n+1 装配/q0 v-c 划分 - - FD 验证矩阵(sym=0 金刚石 2 原子,FD 模板 b0_si_k050_fd2/run_fd.py 派生): - 单 Γ D00 0.0208553 vs FD 0.020854;单 L 0.0129282 vs FD 0.012927(**新增 FD - 基准** b0_si_kLL1_fd);{L,−L} = 单 L 逐位一致(原发散);{Γ,L} 0.0166416 vs - FD 0.016642(原 0.0182462,+9.6%);{L,X}(两不等价非 Γ 点)与权重偏斜 - {Γ,L} 变体全部自洽;ASR 行和全部 ~1e-6 - - 调试方法学沉淀:XB per-(ik,ib) 分解仅 iter-1(v_sc=0)可比但受 GS 采样差异混淆; - ASR 行和 = 免 FD 的在跑检测器;跨采样对比仅 {L}vs{L,−L}(BZ 等价)合法 - - 回归 14/14(CELL 4 + DFPT 8 + IO 2);治理仅既有 header/docs WARNING - - 遗留:调试插桩(OCCCHK miss/集合计数、PTCHK/DYNCHK2/4/XB/MDBG/JPROBE)保留至 - B0 收尾统一清理(用户决策);真正的金属分数占据 DFPT(de Gironcoli 成对方程) - 超出当前绝缘体范围,dfpt_metal 占位 - - [x] 验证梯队扩展(单 k 0.25 / k 网格 / 金属占据区界 / ε∞Z* / MPI 冒烟)`(本轮)` - - **新增 FD 通过项**:单 k=(0.25,0,0)(Λ 点,392 球)D 行 0 实部 vs FD 全部 ~6e-7 - (0.0104952/0.00304483/−0.0104942 vs 0.010495/0.003044/−0.010495);单边 k 采样的 - Hermitian 虚部(D(0,3) imag 0.0061)= X_ba≠X_ab 的预期产物,物理力常数取实部 - (FD 证实);2×2×2 非移位网格(σ=0.005 绝缘区)D00 0.0127458 vs FD 0.012739 - (0.05%),非对角/ASR 精确 - - **金属占据区界定量**(2×2×2 网格,Γ VBM 带尾):σ=0.015 → VBM 92% 占据,FD 比 - DFPT 软 2.83×(FD 含 dμ/dτ 响应、Sternheimer 流无此通道);σ=0.007 → 99.92%, - 差 3.8%;σ=0.005 → 99.9996%,差 0.05%——残差随带尾权重缩小,dμ 通道缺失的干净 - 指纹。**守卫**:DFPT_PW::init 扫描最终 wg,任一带相对占据落入 (1e-3, 1−1e-3) 即 - WARNING_QUIT(显式拒绝而非静默错值,与 C4 哲学一致;k222 σ=0.015 实测触发) - - **symmetry=1 单 k 陷阱**:k050(0.5,0,0) sym=1 与 sym=0 GS 本征值差 2.6e-3 Ry - (−0.234996 vs −0.237553)→ D 差 0.3-4.4%;sym=0 重跑 = kLL1 逐位一致。FD 基准 - 全部 sym=0,跨 sym 比较非法 - - ε∞/Z*:打印链路通;单 Γ 值(105/55.9)= 长度规范简并分母伪影;8-k 值 - (4.75/6.68)= 采样受限;真验证需密网格(8×8×8 ~10h 串行,推迟为过夜项) - - MPI 冒烟(-np 2,kG):DFPT 相位 MPI_ERR_TRUNCATE 硬崩溃(分布式布局未支持, - 响亮失败无静默错值);Stern CG 标量积无 Allreduce 等串行假设已知,MPI 支持另立 - 工作项 - - 案例清单(/tmp/opencode/):k025/k050(已 sym=0 修正)、k222s007/k222s005(+ - _fd 配对)、gamma_k222_nosym(守卫触发样本)、kG_mpi2(崩溃样本) - - [x] ε∞ 根因修复:compute_eps/compute_born 的 /nk 额外归一 `(本轮)` - - 根因:`wg(ik,v)` 已含完整 k 权重 wk×自旋 2,χ 求和本身即全 BZ 平均;再除 - nk 对 Γ-only 无害(nk=1 掩盖),多 k 时系统性低估(4×4×4 sym1: ÷8、 - 36-k sym0: ÷36) - - 修复:删除 compute_eps 的 `/ nk`(dfpt_q0.cpp:235)与 compute_born 的 - `/ nk`(:299);DFPT_Q0DBG env 探针(p 矩阵 dump)保留供校准 - - 验证:4×4×4 sym1(8 IBZ k)ε∞ 对角均值 = 12.6661;sym0 全 BZ 36 k = - 12.6662(两网格 5 位一致);LDA pz 参考 ~12.7-13.2、实验 11.7 → 定量达标 - - 撤回两条早期误判:①"8π 应为 16π"——wg 的自旋因子 2 已提供 Ry 能量的 - 2 倍换算,8π 正确;②"p 矩阵 ~2.64× 过大"——基于 wfc txt 文件的 - FD/python 复算全部失效(见下条) - - wfc txt 输出不可用于元素级分析(上游 bug 记录,write_wfc_pw.cpp): - G 块按 igl2isz_k FFT-stick 序打印(|G|² 序列 196/410 处下降,非排序), - 系数按 psi-ig 序打印 → 行配对破坏;identity/stable-sort/lex 重配对均 - 无法恢复宇称。判据 = O_h 宇称选择定则(Γ 小群含反演:v=T2g、c=T1u、 - p=T1u;⟨A1g|T1u|T2g⟩ 严格禁戒):文件行配对给出禁戒元素非零 - (kin(0,x-成员)=0.098、kin(v,band7)~0.5),而代码内部 p 矩阵全部禁戒 - 元素精确为零(1e-16)、(v,band7) 非零 = 非局域交换子 [V_NL,r] 项的 - 合法破缺 → 代码 pos_matrix/build_vkb_dk 链路被选择定则整体背书 - - 遗留:sym1 各向异性(ε 对角 13.78/15.34/8.88、非对角 −5.81)= 缺星 - 旋转(IBZ 张量未绕星转动;迹/均值不受影响,星平均投影子对 sym0/sym1 - 两种 k 列表均合法);Z* 均值 15.6 ≠ 参考 ~4.5,除 /nk 外另有独立 bug - (待查,疑与 dV^κ 位移项相关——dfpt_pert.cpp 含另一会话未提交修改, - 需协调) - - [ ] B0 残余:8×8×8 过夜验证(ε∞/Z* 与 D 的密网格收敛);非 Γ q 的物理级验证 - (超胞 FD 或色散对照);插桩清理评审 - - [x] P0-1 未提交修改收编(q≠0 同原子二阶项物理修复 + 调试探针,用户已确认保留路线) - `(完成,commit 2da1a4e83 物理+测试 / 480137167 探针)` - - 背景:工作区 5 文件(dfpt_pert/dfpt_phon/dfpt_pw/dfpt_q0)含两类修改—— - ① 物理修复:同原子 d²V 项两个位移 dressing e^{iq·R} 同乘一原子 → 二阶势 - 携波矢 2q,same-k 期望仅当 2q 折到倒格矢时非零(d2vloc_r 核 w=gcar、 - apply_d2vnl q_eff=fold(2q) + include_middle 门控 |dβ⟩⟨dβ| 中间项、 - accumulate_electron 2q 倒格矢门控);ion_ion 自镜像项去 δ/3 - (D_ii(q)−D_ii(0) 公式经 q 公度超胞 erfc 分裂能量 FD 元素级验证, - α 无关性数值核实);② 调试探针:ZDBG(compute_born 逐 (m,v) 项)、 - BPT(空态 PT 交叉验证 Sternheimer)、NOSC(屏蔽势置零 A/B)、 - D2MID(中间项开关) - - 测试同步(dfpt_phon_serial_test):AccumulateElectronAnalyticContraction 的 - expect 同步 Hermitian 2n+1 累积约定(dc82fac9b)+ 门控语义 - (fixture q=(0.13,0,0.07) 非倒格矢 → d2=0);新增 - AccumulateElectronD2GateOffGenericQ(行 0 纯 cross 锐探针)与 - AccumulateElectronD2CommensurateQ(k=(−½,0,0), q=(½,0,0),三分量 ψ - 钉死 cross 与 d2 核 K_{ab}(G)=−tpiba²·G_a·G_1·Vloc·e^{−i2πG·τ}, - 含 K(G_i−G_j) 负谐波约定:实空间 |u|² 收缩挑出核的负谐波,闭式须跑 - K(G_i−G_j) 配 c_i* c_j) - - **顺序依赖缺陷根除**:psi::Psi 构造只 malloc 不清零("no_record"), - 测试未显式置零的分量读堆垃圾 → 单跑恰逢零页通过、全量套件被前面测试 - 脏堆污染而挂;三处构造后补 psi.zero_out();pert/q0 套件复核已有 - zero_out/全量填充,无同类问题;--gtest_shuffle ×3 稳定 - - 顺手:d2vloc_r 已 (void)q_cart 的遗留参数移除(更新调用点,规则 5) - - 验证:cmake 重配置注册 CELL 测试后 12/12 通过 - (klist/reciprocal_grid/qlist/little_group + DFPT 8 套件);DFPT 串行 - 28/28(phon 9 + pert 8 + q0 5 + rho 6);治理检查 HEAD~2..HEAD 零新增 - ERROR、仅 1 条 docs-sync 警告(无 INPUT 行为变化,无需文档更新) - - **拆分两次提交**(物理修复+测试同步 2da1a4e83;调试探针 480137167), - 本文件回写即本次提交 - - [ ] P0-2 Z* bug 根因与修复(B0 收尾前置,用户已确认优先) - - 现象:Z* 均值 15.6 vs 参考 ~4.5(金刚石);eps 已达标(12.67)→ - pos_matrix/r_mat 链路被选择定则背书,聚焦 compute_born 差异面 - - 首选线索:15.6 ≈ zion(4) + (ε∞−1)·n 的量级 → 疑与 eps 共享因子 - 纠缠(Ω/8π/自旋因子泄漏进 −4 系数链) - - 步骤:① ZDBG 分解 occ-occ vs v→c 块贡献;② BPT 恒等式交叉验证 - dV@q=0 矩阵元;③ 黄金对照 = 小位移偶极 FD(或 Berry 极化)+ - O_h 对称性约束(对角 ~4.5、非对角模式);④ 修复 + - dfpt_q0_serial 增 m 求和结构回归用例 - - **根因判定(完成,两个独立缺陷)**: - 1. **星旋转缺失(已修复,本轮提交)**:sym=1 归约 k 表上直接 - 求和无星平均 → 强各向异性(ε∞ 13.78/15.34/8.88、Z* - 16.72/17.76/12.27)。修复:dfpt_q0 新增 build_stars/ - rotate_tensor/星积分(成员=折叠去重的 kgmatrix 星点, - cart=列形式旋转,atom_map=gmatrix+gtrans 的像原子, - nrotk<=0 或映射失败回退恒等);Z* 部分张量按像原子记账 - (反演伴星换原子,χ₀(−k)=χ₁(k) 已数值验证 2e-5)。 - **关键坑**:`Vector3*Matrix3` 行乘积 G⁻¹KG 是行约定算符, - 张量旋转要列形式 → rotate_tensor 直接用会得 P^T χ P; - {P_m⁻¹k} 是右陪集代表系非左 → 星求和真被破坏(ε∞ 也错), - 必须存转置(dfpt_q0.cpp 构造处已注明) - 2. **公式级缺陷(待修,下一步)**:独立粒子求和 - /(ε_m−ε_v) 缺屏蔽响应(Sternheimer - 2n+1 形式)。金刚石基准(nosym 全网格、星记账正确值): - ε∞=12.6661·δ ✓ 公式正确勿动;Z*=15.5799·δ(ASR 违背: - 电子部分 +11.58/atom vs 应 −4;O_h+反演下金刚石 Z*≡0) - —— plan 早期"参考 ~4.5"来自陈旧日志,金刚石正确目标为 0 - - 修复后验证(zstar_sym3,sym=1 4×4×4):ε∞=12.6661·δ、 - Z*=15.5799·δ,两者均与 nosym 全网格参照逐位一致(非对角 - ~1e-14)→ 星旋转机械精确 - - 新增回归用例 StarRotationCyclicGroup(dfpt_q0_serial):sc 胞 - C3 轨道 3 原子 + k=(¼,0,0),钉死星大小 3、各向异性 - trace-6 张量星平均=2δ、循环 atom_map 覆盖 {0,+1,+2} 位移、 - nrotk=0 恒等回退;build_stars/rotate_tensor/stars_ 移至 - public 供测试 - - 探针:新增 DFPT_STARDBG(build_stars 转储 kgmatrix/gmatrix/ - gtrans/成员 cart/amap),与 ZDBG 等同登记待清理 - - 离线验证工具(/tmp/opencode,不入库):star_check.py/ - star_debug.py/star_compare.py(几何星 vs 代码星逐成员比对, - 定位转置缺陷);truth_check.py(nosym 重建基准) - - **公式级缺陷 v4(完成):Sternheimer 屏蔽 Z\* 全链落地** - - 实现:solve_pos_resp 解 Y^a=(H−ε_v)⁻¹P_c[H,x_a]|ψ⟩(rhs - −(i/tpiba)·dH/dk|ψ⟩,vel=2tpiba²·gk 即 tpiba·dH/dk,恰消 - 归一,QE commutator_Hx_psi 同构;非局域 dk 项 build_vkb_dk - 经 DFPT_DKCHK 中心差分逐 μ 验证);compute_born 收缩 - Z*=zion·δ−2Σwg Re⟨dpsi^κ,scf|Y^a⟩(QE add_zstar_ue 锚定, - m-empty 完备性经 occ-occ pairwise 反对称对消+Sternheimer - 平行输运规范证明);dpsi_disp/pos_resp/set_vsc_r 三套 stash - - 验证矩阵(每项均独立排除一类缺陷):DFPT_XCDBG(f_xc 核 - 中心差分自检,PZ 符号/量级正确);DFPT_XCS 扫描(光滑线性 - 定点);DFPT_ALEG zstar_eu 交叉腿 A==B 逐位一致(收缩/双 - stash 豁免;E 场 SCF 定点与 κ 场自洽);DFPT_PTCROSS 裸交 - 叉谱分解(M⁻¹ 对角+交叉 5 位一致、厄米精确、nb8→16 稳定) - - Berry FD 外锚(本仓 GS berry_phase,δ=0.05 bohr 三结构): - P(u) 斜率 <1e-5 → Z*≡0 目标由独立机制确认 - - **外部锚点战役(QE 7.2 本地串行构建,同 UPF/胞/ecut/网格)**: - GS 能量逐位一致(−215.426 eV);Γ 声子 TO 517.63 vs 我们 - 517.5/517.606/517.722(0.03%);Z*=−1.19765 vs 我们 −1.19928 - (0.1%);ε∞=23.668=2×我们的 11.341+1(0.05%) - - **唯一真 bug:ε∞ 缺因子 2(已修)**:QE dielec.f90 实锚 - ε=δ−4·(4π/Ω)·wk·Re⟨Y^i|dpsi^E,j⟩=16π/Ω 形;compute_eps 与 - ALEG 探针同步 8π→16π,ComputeEpsTwoLevelAnalytic 期望值同步, - 串行 6/6 - - **s=1.29982 之谜消解**:非代码缺陷。QE 判别实验(换赝势 - ONCV@4×4×4 仍 23.49/−1.169;加密 pz-vbc@8×8×8 → 14.04/ - −0.092→0)证明 Γ 中心 4×4×4 网格收敛误差为两代码共享的 - 物理量级;κκ 干净(s⁰)、κE ×s、EE ×s² 的通道指纹即 - Y 族对网格收敛的敏感性分层 - - **验收口径修订**:4×4×4 下验收值=QE 同网格参照 - (Z*≈−1.20·δ、ε∞_scf≈23.68、TO 517.5±0.2、声学 |·|<20); - Z*→0 对称性目标移交 P0-3 8×8×8 过夜(QE@8×8×8 已预示 - −0.09→0) - - 探针登记(P0-3 清理评审):DFPT_ALEG(solve_efield_resp+ - aleg_crosscheck+PTCROSS,dfpt_pw.cpp)、DFPT_XCS/XCDBG/ - DKCHK/NOXC/NOSC/YCHK/BPT/MDBG/JPROBE;dpsi_efield stash - (dfpt_pw_data)随 ALEG 保留 - - [ ] P0-3 B0 收尾:8×8×8 过夜(ε∞/Z*/D 密网格收敛);sym1 星旋转各向异性处理或记录在案 - (非 Γ q 物理级验证已由 QE 直接锚定完成:L 点 0.1–1.1%、bare 链 - 0.008–0.4%,见下方缺陷修复条目;超胞 FD 参照降级为可选项) - - **compute_eps SCF 化(完成,98c7f114c)**:solve_efield_resp - 转正(QE solve_e 顺序:Y 腿后、位移 solve 前); - compute_eps 改消耗 pos_resp+dpsi_efield 收缩 - ε=δ−(16π/Ω)Σwg Σ_occ Re⟨Y^a|dψ^E,b⟩(dielec.f90 锚,星平均 - 保留);PT r-matrix 路径退役(pos_matrix 保留为解析参照); - ComputeEpsScfSyntheticStash 替换 PT 用例,串行 6/6; - 端到端 sym 4×4×4 ε∞=23.35·δ(原 IPA 12.67),与 nosym - ALEG 23.68、QE 23.67 同源 - - **[缺陷已修复] 非 Γ q 全链路错误 → 根因=drho 缺自旋因子 2 - (a915352cd)**: - - 根因:compute_drho 用 w/Ω 且仅在 Γ 的 Hermitian 补全里补 - 2Re;QE incdrhoscf 在一切 q 用 wgt=2·w/Ω(自旋简并因子, - 非 Hermitian 补全)。q≠Γ 屏蔽强度减半 → L 点频率塌到 - −948/−148×2/183/199×2。修复后 w1=2w/Ω 恒定、Γ 补全只取 Re - (Γ 代数等价不变) - - 修复后验证(Si NC pz-vbc,4×4×4,24³ 网格):q=L(−0.5,0.5,0.5) - 2π/a → 100.49×2/380.41/402.11/485.93×2 vs QE - 101.61×2/380.54/402.24/486.28×2(0.1–1.1%);Γ 保持 - 517.491(QE 517.633);bare(NOSC vs QE niter_ph=1) - −2281.83/−578.00×2/−528.59/−278.82×2 vs −2282.01/ - −577.16×2/−527.51/−277.69×2(0.008–0.4%)→ 裸链 - (dV/dψ/term2/term3/Ewald)整体正确 - - 排除过程存档:dVnl 链逐位复现(becp/dcbecp/term_a/term_b - vs dvqpsi_us_only.f90,relmax≤2.3e-15);vkb/Simpson/radial_vq - 复现(≤4.4e-16);CG 残差直接证明 P(H−εS)P·dψ=P_c·rhs - (~6e-9);QE D(L) 从 matdyn 本征集重建(U 全实 → D 实对称, - 逐元素 diff 定位 ele 全错、ion 正确);NOSC 对照 QE bare - 把嫌疑压缩到屏蔽链 → drho 归一化 - - 遗留:0.1–1.1% 残差呈均匀绝对 D 误差(最小 ω 相对误差最大) - —待查(k 权重/FFT 细节);QE fildrho 记录与我们的 drho 逐 - 元素对照通道未打通(模式→笛卡尔重建后 G 键匹配失败, - 疑记录顺序/约定,未继续);DFPT_RHSDUMP/NLDUMP/MDBG 探针 - 已从 dfpt_pert.cpp 清除(DFPT_DPDUMP/DRHODIR 亦然), - dfpt_pw.cpp 的预存探针(NOSC/JPROBE/BPT/XCS/NOXC/YCHK/ - PTCROSS/DKCHK/ALEG/MIX_BETA/MDBG)未动 - - **[旧缺陷登记归档] 非 Γ q 全链路错误(修复前记录,见上)**: - QE 7.2 本地参照(同 UPF/胞/ecut/4×4×4 网格): - q=L(0.5,0,0) → 125.39×2/239.10/473.79×2/496.34 cm⁻¹; - q=Γ-L 1/4 → 102.75×2/144.31/494.62×2/502.63。我们: - L 点 2-k 显式 → −1170.9/−296.2×2/292.5/560.5×2(简并 - 结构 1+2+1+2 正确、幅度全错);L 点 64k(sym−1) → - −948/−148×2/183/199×2;q=1/4 64k → −1474/−1038×2/ - −957×2/−954(全虚频)。**早期 b0_si_qL"验证"结论作废** - (当时无外部参照,q↔−q 0.2-1% 一致本身即误差信号) - - 已排除:屏蔽装配(DFPT_NOSC 下 2-k bare 也错 −3291); - H(k+q) 组装(DBG ⟨ψ|H(k+q)|ψ⟩=GS eig 到 2e-6); - apply_vr_core 卷积约定(PW_Basis_K 实空间为周期 u_k, - 纯 G 卷积 + e^{i(Δ+q)τ} 相位数学正确);DFPT_KQ_Basis 球 - 选择(与 GS 同球一致);d2ionq Ewald(有 FD 验证注释+用例) - - 存活嫌疑:dVloc_dtau(dVnl_dtau) 的 (Δ+q) 系数链在真实 - case 的实现(C1 fixture 测试过的约定可能未覆盖全网格 - dn≠0 标签折叠路径);build_occ_kq 的 dn≠0 G 向量匹配 - (2-k case dn=(1,0,0) 当时验证过,64k 大量 dn≠0 未验); - term2 cross 的逐元素正确性(DFPT_XB 打印未对照) - - 8×8×8 nosym ALEG 验收运行中(~4h,q=0 不受本缺陷影响) - - [x] 非 Γ q 路径冒烟 `(本轮,b0_si_qL/qmL)` - - dfpt_qfile + QList::read_from_file 端到端首次运行:q=(0.5,0,0),k={Γ,L}, - compute_q0=false/loto=false;6 位移全收敛、无 NaN、D Hermitian - - **dn≠0 经 q 的折叠实战**:Γ+q→L(dn=0)、L+q→Γ(dn=(1,0,0),383/411 球)—— - 标签匹配 miss=0,整数三元组机制在真 q 下工作 - - **q↔−q 一致性**:D(−L) 特征值与 D(+L) 一致到 0.2-1%(−0.07621/−0.05528/ - −0.03265/−0.02005/−0.00816/+0.06802 vs −0.07607/−0.05518/−0.03249/ - −0.01996/−0.00808/+0.06822)——稀疏 2-k 采样下 q=±L 采样不同跃迁集, - 接近一致即内部自洽;4 个负本征值(虚频)为 2-k 超稀采样的性质,非 q 路径 - bug(±q 忠实重现);物理级验证需密网格/超胞 FD - - [x] B2 输出正式化 `(本轮)` - - 多 q 频率报告:DFPT_Phon::format_q_report(每 q 一块,表头带 - direct q 坐标,模式行定点 6 位小数);esolver run_post_process - 循环 get_nq() 输出,tensor 块仅在已计算时打印(compute_q0=false - 不再输出空表头) - - LO-TO 修正频率:DFPT_Phon::diagonalize_loto(add_loto 后对 - 修正 dynmat(0) 再对角化,存 phon_freq_loto,原 phon_freq(0) - 不动);format_loto_report 沿数据层方向输出(无修正频率时 - 返回空串) - - loto 方向经数据层:DFPT_PW_Data::loto_dir_(默认 (1,1,1)/√3, - setter 归一化、零向量保持原值);DFPT_PW::set_loto_dir/ - get_loto_dir 公开 API;run() 中硬编码删除,改用 - data_.get_loto_dir()(一般方向控制随 A 阶段 irrep 机制) - - 格式回归测试:phon 串行 3 用例(LotoDirNormalization、 - DiagonalizeLotoClosedForm——xx 2×2 块 {0,13/12·pref} 闭式、 - FormatReportsRegression——逐字符字符串钉死),12/12 - - 端到端冒烟(Γ, compute_q0+loto, 4×4×4):TO 517.490709×3 - 不变;LO-TO 块沿 (0.577350 0.577350 0.577350) 输出,声学支 - −7.325→+73.208 cm⁻¹、光学支不动;ε∞=23.6825、 - Z*₁=Z*₂=−1.19928δ。**观察登记(非缺陷)**:QE 同 setup 自身 - 打印 Z*₁=Z*₂=−1.19765(asr 前 Sum=−2.395,asr 后全零), - 我们逐值一致(0.13%,与 D 同残差量级);同号 Z* 经键心反演 - 对称性成立(F₁(E)=−F₂(−E) → Z₁=Z₂),故 LO-TO 抬升的是 - 声学支组合——与 QE 输入自洽。ΣZ* 求和规则的表述与 asr 语义 - 留待后续物理阶段讨论 - - [x] B3 Kerker 预条件混合(2026-08-24 完成) - - DFPT_Rho 内自实现 |G+q|²/(|G+q|²+a²) 预条件(不引 charge_mixing.h); - mix_type 支持 plain/kerker;验收:λ_A1≈−2.2 模型问题 β=0.7 收敛 - (JPROBE 复用)、金刚石频率与 β 无关、默认 β 回调并文档记录 - - 实现:`DFPT_Rho::init` 增 `mix_type`("plain"/"kerker") 与 `kerker_a2` - (1/lat0² 单位,与 |G+q|² 同纲);筛选 f_g=|G+q|²/(|G+q|²+a²) 用 - v_hartree_q 同一约定(gcar+q_frac·G);"screen 双方→plain_mix→补回 - 被筛部分"得 mixed=rin+βf(out−rin)(QE 语义:存量密度保持物理量, - 非筛选缩放),|G+q|=0 谐波冻结(与 compute_drho 丢弃一致)。接线层 - env `DFPT_MIX_TYPE`/`DFPT_KERKER_A2`(设计期校准旋钮,镜像 - DFPT_MIX_BETA 先例;默认 plain → 行为逐字节不变,默认 β=0.4 回调 - 见 dfpt_pw.cpp init 注释与 dfpt_rho.h 类注释) - - 串行验收(新增 2 用例,rho serial 6→8):解析首步 - mixed=β·f·out;模型问题 out=D·in+s(最小 |G+q| 壳 D=λ_A1=−2.2, - 其余 0.3)β=0.7:plain 残差>1 发散、kerker(a²=9w2_min) <1e-8 - 收敛到 target —— 无需 JPROBE(DFPT_DEBUG 残差轨迹即可作证), - JPROBE 可进入清理队列 - - 端到端(L 点, 4×4×4):plain β=0.7 残差振荡 >1、|drho|→1e20 - 爆发散;kerker β=0.7 收敛(1393 s,138 SCF 迭代/6 位移,反快于 - plain β=0.4 的 2332 s);频率与 (mix_type, β) 无关——三配置 - 100.487828²/380.41384/402.10912/485.93199² 一致至 8–9 位 - (参考 plain β=0.4:100.488/380.414/402.109/485.932) - - 附带修复(陈旧二进制掩盖的 a915352cd 遗留):rho serial 测试 - kq0.init 未跟进 4 参签名、brute-force 参考缺自旋因子 2 —— - 四个串行二进制全部重建后 pert 8/phon 12/q0 6/rho 8 通过, - ctest 12/12 - - [x] B4 数据层收编(2026-08-24 完成) - - 收敛台账(converged_/residuals_/current_iter_ 按 (q,irrep))并入 - DFPT_PW_Data;删除 DFPT_IrrepData 适配层与 get_dpsi_obj static dummy; - 测试迁移;保留 (q,irrep) 接口形状;run() 外层 while 记账语义梳理 - - 台账:DFPT_PW_Data 单槽(set_current_iter(int) 等,生产代码只写 - 不读)替换为 (q,irrep) 键控六访问器(map 值成员,缺键读 - false/空/0,clean() 清空);solve_displacement / - solve_efield_resp 内层只写语句删除——位移级状态回归函数局部量, - 末残差经返回值交 run() 聚合 - - 适配层:dfpt_irrep_data.{h,cpp} 删除(git rm);其 irrep==0 转发 - 的 dpsi/drho/dv 访问本就是数据层既有 API;get_dpsi_obj 无调用者 - 纯删 - - run() 记账语义:外层 while 每遍 current_iter+1(原先不递增、 - 无条件置 converged 的退化单遍改为诚实台账)——一遍 = 3N 位移各自 - 完整收敛 + 2n+1 累积,遍残差取各位移末残差最坏值,worst< - conv_thr 才置收敛;未收敛遍会重启全量求解(solve_displacement - 从零输入起),max_iter_ 界内诚实重试,残差史留痕。收敛工况下 - 行为与旧版逐位一致 - - 测试迁移:dfpt_irrep_data_test.cpp → dfpt_pw_data_test.cpp - (目标 MODULE_DFPT_pw_data_test,5 用例:QList 委托、边界安全 - ((q,spin) 二参签名)、roundtrip、键控台账独立性+clean() 复位、 - U0 预留);两处 CMakeLists 同步(含 pw_run_test 源列表去 - irrep_data) - - 验证:ctest 12/12(pw_data_test 顶替 irrep_data_test 槽位); - 4 串行套件 pert 8/phon 12/q0 6/rho 8;端到端 L 点默认配置 - 冒烟逐位复现参考频率(见提交) - - [x] 插桩清理评审(B0/B3 后统一,2026-08-25 完成) - - 已删(验收全毕的设计期仪器):PTCHK(规范检查 + 8 带 term2 交叉 + - HF/de_code 通道定位)、DYNCHK 全家(term2/d2gate/d2k/d2/ion/ele/ - elei/DYNCHK4 双 zheev 对照)、MDBG(drho_iters 二进制倾倒 ×2)、 - JPROBE+JPROBE_NOXC(B3 验收后按计划删)、OCCCHK(含 dbg_miss 标签 - 分析与 empty_kq_/empty_kq_eig_ 伴生存储)、XB、BPT(含 want_empty - 投影子扩张)、NOSC、XCS/NOXC(v_sc 组装简化为无旋钮路径)、DKCHK、 - YCHK、D2MID(include_middle 收编为字面 true,q 无关性已定案)、 - ALEG+PTCROSS(aleg_crosscheck 方法整体删除)、STARDBG、Q0DBG、 - drho_dfpt.dat dump;连带清理死累加器(d2sum_loc/nl、cross_k)与 - 失用途头文件(pw.cpp 的 /) - - 保留:DFPT_DEBUG(solve 循环残差轨迹 + posresp 追踪,B3/B4 验收 - 仪器,日常收敛诊断);DFPT_MIX_BETA/DFPT_MIX_TYPE/DFPT_KERKER_A2 - (B3 设计期校准旋钮,init 注释已文档化) - - 验证:ctest 12/12、串行 pert 8/phon 12/q0 6/rho 8;默认路径行为 - 不变(删除项全部 env 门控默认关;include_middle/want_empty 默认值 - 与收编值一致)——端到端 L 点默认配置冒烟逐位复现参考频率 - (100.487828/100.487829/380.413847/402.109158/485.931988×2) -- [ ] A irrep 分解(保留接口,工程验证完成后立项) diff --git a/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md b/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md deleted file mode 100644 index 9288865e630..00000000000 --- a/source/source_pw/module_dfpt/PLAN_reciprocal_grid_refactor.md +++ /dev/null @@ -1,116 +0,0 @@ -# 抽象倒空间基类(K/Q 点统一)重构计划 - -- 状态:已批准(2026-08-14) -- 关联模块:`source/source_cell`、`source/source_pw/module_dfpt` -- 背景:DFPT 需要 q 点管理。q 点与 k 点大量共用倒空间网格/坐标/归约逻辑, - 但有两点关键差异:(1) q 点不涉及自旋;(2) q 点需要小群不可约表示分解。 - -## 目标 - -提取一个抽象的倒空间基类 `ModuleCell::ReciprocalGrid`,让 `K_Vectors` 与 -`QList` 分别继承其共用功能,再各自加入不共用的功能: - -- 基类:网格生成(Monkhorst-Pack)、坐标/权重、star 归约原语,spin-free。 -- `K_Vectors`(继承基类):自旋展开(isk、nspin 翻倍、SOC/磁群)、kstars、 - k 特有 MPI 分发。 -- `QList`(继承基类):q 网格生成、小群不可约表示(接口占位→完整实现)。 - -## 关键差异与决策(已与作者确认) - -1. q 不含自旋:第一阶密度是标量,q 作为 `nspin=1` 的纯列表;自旋相关逻辑 - 全部下沉到 `K_Vectors`,基类不触碰。 -2. q 需要两级归约: - - **star 归约**:与 k 完全相同(含 `q≡-q` 的 time-reversal 加倍,`kvec_ibz_kpoint` - 已自带)。 - - **小群不可约表示分解**:每个 q 的小群 `{R|t}: Rq≡q+G`,将 3N 原子位移分解 - 到各 irrep 的代表模,只解代表模。仓库无现成实现,需新增。 -3. 对称性模块已提供所需全部输入:`symm.kgmatrix[48]`(K 空间旋转)+ - `symm.gtrans[48]`(分数平移,供 `e^{-iq·t}` 相因子)。 - -## 类体系设计 - -``` -ModuleCell::ReciprocalGrid(新抽象基类,spin-free,放 source_cell/) - ├─ 数据(protected+getter): kvec_c/kvec_d/kvec_c_full, wk, ngk, nmp[3], - │ kl_segids, kc_done/kd_done, nks/nkstot/nkstot_full, nspin=1, is_mp - ├─ 方法: renew, Monkhorst_Pack(+formula), set_both_kvec, normalize_wk, - │ print, reduce_ibz(star 归约原语) - └─ 纯虚钩子: virtual void reduce_by_symmetry(...) = 0 - ↑ ↑ - K_Vectors(public 继承) QList(public 继承) - ├─ isk, nspin, set_kup_and_kdw ├─ generate_mesh / read_from_file - ├─ kstars, ibz_index ├─ reduce_by_symmetry = star 归约(复用原语) - ├─ kvec_mpi_k(含 spin 广播) └─ get_irreps = 小群 irrep 分解(新增) - └─ reduce_by_symmetry 覆写 └─ nirr_ / irrep_modes_ - -ModuleSymmetry::LittleGroup(新独立组件,放 module_symmetry/) - └─ 输入 kgmatrix+gtrans+q,输出小群操作/irrep 模式;QList 聚合它 -``` - -要点: -- 基类不含自旋、不含 irrep;irrep 用独立组件类,避免把 q 特有复杂度带进基类。 -- 基类显式传参、不新增 `GlobalV` 依赖(AGENTS.md 规则 1)。 -- `K_Vectors` public 继承且不改外部 API(`kv.kvec_d` 等按名访问全部兼容)。 - -## 实施阶段 - -### Phase 1 — 提取抽象基类 + K_Vectors 迁移(行为不变) -1. 记录基线:`ctest` 现有 `klist_test`/`klist_test_para`/`parallel_kpoints_test`。 -2. 新增 `source/source_cell/reciprocal_grid.h/.cpp`(命名空间 `ModuleCell`): - - 迁移 K_Vectors/KVectorUtils 中 spin-free 的网格生成、坐标转换、 - 权重归一化、打印逻辑(逐行一致)。 - - `reduce_ibz(...)`:`kvec_ibz_kpoint` 的通用内核(restrict + MP k-lattice - 转换 + 旋转等价判断 + 权重计数 + `-q` 加倍)。 - - 纯虚 `reduce_by_symmetry(...)`。 -3. `K_Vectors` 改为 `public ModuleCell::ReciprocalGrid`: - - 私有保留:isk、koffset、k_kword、k_nkstot、kstars、ibz_index、para_k。 - - 覆写 `reduce_by_symmetry()`:构造 kgmatrix(含 nspin=4 磁群分支 + - include_inv 加倍 + kstars),调基类 `reduce_ibz`,再执行 K 特有 - `update_use_ibz`(nspin 扩容)。 - - `set()` 改为调用覆写方法。 - - `KVectorUtils` 自由函数先保留为薄封装委托基类(保住 - `esolver_fp.cpp:178` 的 `set_after_vc` 与现有测试编译),随后测试迁移、 - 删封装。 -4. `source/source_cell/CMakeLists.txt` 接入新文件。 -5. 回归:重构后重跑基线测试,输出必须一致;不一致立即回退对应文件。 - -### Phase 2 — QList 接入基类 -- `class QList : public ModuleCell::ReciprocalGrid`。 -- `generate_mesh`:基类 `Monkhorst_Pack` 建 q 网格 → `reduce_by_symmetry()` - (恒加 `-q`,无磁群)→ 归约后补 `kvec_c`(笛卡尔坐标)→ 权重归一化 - → `use_irreps` 开关控制是否填充 `nirr_`/`irrep_modes_`。 -- 保持 `get_nq/get_q/get_nirr/get_irrep_modes` 接口不变;删除 design-phase 桩注释。 -- q 点补充功能(已完成): - - `read_from_file`:读 q 点文件(Gamma/Monkhorst-Pack 网格、Direct/Cartesian - 列表、Line_Direct/Line_Cartesian 插值路径),不做对称归约(接口无 symm)。 - - `print_qlists`:打印 q 点笛卡尔/直接坐标表(`Q-POINTS` 标签)。 - - `get_nirr/get_irrep_modes` 越界安全;`use_irreps=false` 时 irrep 数据为空。 - - `nkstot_full` = 归约前网格规模;`wk` 求和 = 1(网格/列表)或逐点 1(路径)。 - -### Phase 3 — 不可约表示接口(module_symmetry,先留接口) -- 新增 `source/source_cell/module_symmetry/little_group.h/.cpp` - (命名空间 `ModuleSymmetry`),QList 聚合。 -- 首版仅接口:`set_q(q,symm)`、`get_nirr()`(返回 1,全对称 A1)、 - `get_mode_basis(irrep)`、`get_little_group_ops()`。 -- 完整 irrep 表/投影算符下一轮实现,配金刚石/闪锌矿 q=Γ/X/L 已知 irrep 表单测。 - -### Phase 4 — DFPT 接线(后续迭代) -- `DFPT_PW::init` 真正走 `generate_mesh`;`DFPT_PW_Data` 用 - `get_nirr`/`get_irrep_modes` 驱动逐 irrep SCF。 - -## 测试策略 - -- 新增 `source/source_cell/test/reciprocal_grid_test.cpp`:MP 生成、d/c 转换、 - 权重归一化、`reduce_ibz` 在已知小群(fcc、金刚石)上的归约结果。 -- 新增 `source/source_cell/test/qlist_test.cpp`:q 网格生成、star 归约(含 `-q`)、 - `get_nirr` 接口。 -- 回归基准:现有 `klist_test`、`klist_test_para`、`parallel_kpoints_test` - 输出逐字节一致。 - -## 风险与边界 - -1. **K_Vectors 行为回归**:靠基线测试锁定;提取中任何逻辑漂移立即回退。 -2. **命名空间**:基类放 `ModuleCell`,K_Vectors 保持全局命名空间继承 - (跨命名空间继承合法);彻底统一命名空间列为后续清理项。 -3. **`ngk` 归属**:PW_Basis 填充的每点平面波数,k/q 都需要,放基类。 -4. **C++11 基线、LF 换行、新文件进 CMakeLists**:全程遵守 AGENTS.md。 diff --git a/source/source_pw/module_dfpt/README.md b/source/source_pw/module_dfpt/README.md deleted file mode 100644 index 9b5eae426f1..00000000000 --- a/source/source_pw/module_dfpt/README.md +++ /dev/null @@ -1,116 +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; -// dftu is a const Plus_U* wired by the esolver layer ONLY when dft_plus_u -// is enabled; pass nullptr otherwise (DFPT never reads PARAM itself). -dfpt.init(ucell, psi, pw_rho, pw_wfc, sf, veff_r, wg, eig, xc, nelec, ecutwfc, dftu); -dfpt.set_qmesh(4, 4, 4); -dfpt.set_conv_thr(1e-8); -dfpt.set_mix_beta(0.4); -dfpt.set_compute_q0(true); -dfpt.set_loto(true); -dfpt.run(); - -// Get results -std::vector freq = dfpt.get_phonon_freq(q_idx); -ModuleBase::matrix eps = dfpt.get_dielectric_tensor(); -``` - -The production wiring is INPUT-driven (`esolver_type dfpt` + the `dfpt_*` -parameters in INPUT); see `docs/advanced/input_files/input-main.md`. - -## 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 From 469cdbf99d502dae731c62b529e1c185badfb565 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 13:33:04 +0800 Subject: [PATCH 40/50] Fix: adapt DFPT to the refactored Plus_U interface (compile break + U guard) The develop-side DFT+U refactor (#7852-#7867) removed source_lcao/module_dftu/dftu.h and the is_locale_initialized() member, which broke every CMake build configuration of this branch at dfpt_pw_data.cpp (all 9 CI build variants plus Test/CUDA/abacuslite failed at the compile step; only the Makefile job passed because the Makefile.Objects DFPT entries were absent at that merge point). Changes: - DFPT now consumes the PW-side Plus_U_Base (source_pw/module_pwdft/ dftu_base.h) instead of the LCAO-side Plus_U header: dftu_ member, DFPT_PW_Data::init / DFPT_PW::init signatures and get_dftu() all use const Plus_U_Base* (the esolver call site passes &this->dftu with an implicit upcast). This removes the PW -> LCAO cross-layer include. - u_active() = with_u() && is_occ_mat_initialized(): the reservation usability now follows the occupation-matrix state of the provider. - DFPT_PW::init rejects a wired provider explicitly (WARNING_QUIT): the ground state supports PW-basis DFT+U now, but every DFPT U hook (cal_docc, build_dv_u, dftu_onsite, born/docc contractions) is a no-op U0 reservation, so running anyway would silently drop the whole first-order U response (fail-loud, same pattern as the metallic-sampling guard). - test/dftu_test_support.cpp rewritten: the old static-member replicas no longer exist; the shim now provides only the Plus_U_Base ctor/dtor (also linked into MODULE_DFPT_pw_data_test, which constructs the provider directly). dfpt_pw_run_test's locale test becomes a death test pinning the WARNING_QUIT guard; the with_u/u_active contract moved to DFPT_PW_DataTest.DftuReservationProviderUsability; the unused dftu.h includes dropped from the phon/q0 serial tests. Verification (GNU 8.3.1 + OpenMPI 5.0.3, GCC13 no-MPI cross-check): - cmake --build build --target abacus_pw_para: builds/links - cmake --build build-nompi (-DENABLE_MPI=OFF -DENABLE_LCAO=OFF) --target abacus_pw_omp: builds/links - ctest -R 'MODULE_DFPT|MODULE_CELL': 50/50 pass (incl. the new death test and provider-usability case); ./build/abacus_pw_para --version prints v3.11.0-beta8 - agent_governance_check --staged: no findings --- source/source_pw/module_dfpt/dfpt_pert.cpp | 11 +++-- source/source_pw/module_dfpt/dfpt_pw.cpp | 18 +++++++- source/source_pw/module_dfpt/dfpt_pw.h | 10 ++-- source/source_pw/module_dfpt/dfpt_pw_data.cpp | 11 +++-- source/source_pw/module_dfpt/dfpt_pw_data.h | 20 ++++---- .../source_pw/module_dfpt/test/CMakeLists.txt | 2 + .../module_dfpt/test/dfpt_pw_data_test.cpp | 28 +++++++++++ .../module_dfpt/test/dfpt_pw_run_test.cpp | 39 ++++++++-------- .../module_dfpt/test/dftu_test_support.cpp | 46 ++++++------------- .../test_serial/dfpt_pert_serial_test.cpp | 9 ++-- .../test_serial/dfpt_phon_serial_test.cpp | 1 - .../test_serial/dfpt_q0_serial_test.cpp | 1 - 12 files changed, 113 insertions(+), 83 deletions(-) diff --git a/source/source_pw/module_dfpt/dfpt_pert.cpp b/source/source_pw/module_dfpt/dfpt_pert.cpp index 8bb071a0b3a..dbd58c8491c 100644 --- a/source/source_pw/module_dfpt/dfpt_pert.cpp +++ b/source/source_pw/module_dfpt/dfpt_pert.cpp @@ -603,11 +603,12 @@ void DFPT_Pert::dVnl_dtau(int atom_idx, int dir, 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 the LCAO orbital files were loaded - // (u_active()). A pure-PW run wires Plus_U non-null but the locale is not - // initialized, so no DFT+U term can be assembled yet; the diamond DFT+U - // test (C7) will exercise this path once OnsiteProjector integration on - // the DFPT k+q basis is finalized. + // 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; } diff --git a/source/source_pw/module_dfpt/dfpt_pw.cpp b/source/source_pw/module_dfpt/dfpt_pw.cpp index 7197f3d5682..ecc68474686 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw.cpp @@ -61,7 +61,7 @@ class DFPT_PW::Impl { const XC_First_Order* xc_ = nullptr; double nelec_ = 0.0; double ecutwfc_ = 0.0; - const Plus_U* dftu_ = nullptr; + 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) @@ -109,7 +109,7 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, 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* dftu) { + double nelec, double ecutwfc, const Plus_U_Base* dftu) { pimpl_->ucell_ = &ucell; pimpl_->gs_psi_ = psi; pimpl_->pw_rho_ = pw_rho; @@ -149,6 +149,20 @@ void DFPT_PW::init(UnitCell& ucell, const psi::Psi>& psi, pimpl_->ecutwfc_ = ecutwfc; 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); diff --git a/source/source_pw/module_dfpt/dfpt_pw.h b/source/source_pw/module_dfpt/dfpt_pw.h index c1258133712..59a2dbb1fdb 100644 --- a/source/source_pw/module_dfpt/dfpt_pw.h +++ b/source/source_pw/module_dfpt/dfpt_pw.h @@ -17,7 +17,7 @@ #include #include -class Plus_U; +class Plus_U_Base; class Structure_Factor; namespace ModulePW { @@ -55,14 +55,16 @@ class DFPT_PW { 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* dftu); + 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 (locale initialized, i.e. the LCAO - /// orbital files are present). + /// 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; diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.cpp b/source/source_pw/module_dfpt/dfpt_pw_data.cpp index e3fe114bcda..fc8bea0d747 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.cpp +++ b/source/source_pw/module_dfpt/dfpt_pw_data.cpp @@ -7,7 +7,7 @@ // ============================================================ #include "dfpt_pw_data.h" -#include "source_lcao/module_dftu/dftu.h" +#include "source_pw/module_pwdft/dftu_base.h" #include @@ -20,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, const Plus_U* dftu) { + int nrxx, int nspin, int nat, const Plus_U_Base* dftu) { qlist_ = qlist; nk_ = nk; nbands_ = nbands; @@ -40,9 +40,10 @@ void DFPT_PW_Data::clean() { } bool DFPT_PW_Data::u_active() const { - // locale initialization requires the LCAO orbital files; a pure-PW run - // without them has dftu != nullptr (wired upstream) but is not usable. - return with_u() && dftu_->is_locale_initialized(); + // 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) { diff --git a/source/source_pw/module_dfpt/dfpt_pw_data.h b/source/source_pw/module_dfpt/dfpt_pw_data.h index 1e244116362..6ad7656ec18 100644 --- a/source/source_pw/module_dfpt/dfpt_pw_data.h +++ b/source/source_pw/module_dfpt/dfpt_pw_data.h @@ -19,7 +19,7 @@ #include #include -class Plus_U; +class Plus_U_Base; namespace ModuleDFPT { @@ -46,7 +46,7 @@ class DFPT_PW_Data { ~DFPT_PW_Data(); void init(ModuleCell::QList* qlist, int nk, int nbands, int npw_max, - int nrxx, int nspin, int nat, const Plus_U* dftu); + int nrxx, int nspin, int nat, const Plus_U_Base* dftu); void clean(); @@ -144,15 +144,15 @@ class DFPT_PW_Data { /// 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* - /// only then. - /// with_u(): a Plus_U provider is wired (dft_plus_u enabled upstream). - /// u_active(): the provider is additionally usable (locale initialized, - /// which requires the LCAO orbital files; a pure-PW run - /// without them must degrade to inactive safely). + /// 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* get_dftu() const { return dftu_; } + 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. @@ -236,7 +236,7 @@ class DFPT_PW_Data { double dmu_ = 0.0; /// DFT+U reservation state (U0) - const Plus_U* dftu_ = nullptr; + const Plus_U_Base* dftu_ = nullptr; std::vector>> docc_; /// converged v_sc per displacement (atom, dir): [3*nat] entries diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index eb16ac1c1a7..047f5906bcb 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -11,6 +11,8 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp + # Plus_U_Base ctor/dtor support shim (see dftu_test_support.cpp). + dftu_test_support.cpp ) AddTest( 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 index 6c564ec1d68..9bfa0ba6ae8 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp @@ -14,6 +14,7 @@ #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" pseudo::pseudo() { @@ -299,3 +300,30 @@ TEST_F(DFPT_PW_DataTest, DftuReservationWithNullProvider) 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 index 6b89ce90939..6934097d067 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -14,7 +14,7 @@ #include "source_base/parallel_global.h" #include "source_base/global_variable.h" #include "source_estate/module_charge/charge_mixing.h" -#include "source_lcao/module_dftu/dftu.h" +#include "source_pw/module_pwdft/dftu_base.h" #include "source_pw/module_dfpt/dfpt_pw.h" pseudo::pseudo() @@ -212,23 +212,26 @@ TEST_F(DFPT_PWRunTest, DielectricAndBornAreExposed) EXPECT_EQ(born.nc, 0); } -TEST_F(DFPT_PWRunTest, DftuReservationWithProviderButUninitializedLocale) -{ - // DFT+U reservation (U0): a non-null Plus_U is wired (dft_plus_u enabled - // upstream) but its locale is NOT initialized here because the LCAO - // orbital files are absent. with_u() must be true, u_active() must be - // false (safe pure-PW degradation), and run() must complete without - // touching any DFT+U kernel (all U hooks are no-op stubs). - Plus_U dftu; +TEST_F(DFPT_PWRunTest, DftuReservationWithProviderRejectsInit) +{ + // 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; - dfpt.init(ucell, psi, nullptr, nullptr, nullptr, std::vector(), - ModuleBase::matrix(), ModuleBase::matrix(), nullptr, 1.0, 15.0, &dftu); - EXPECT_TRUE(dfpt.get_with_u()); - EXPECT_FALSE(dfpt.get_u_active()); - dfpt.run(); - - // still produces the expected number of phonon modes per q - const int expected_modes = 3 * ucell.nat; - EXPECT_EQ(dfpt.get_phonon_freq(0).size(), expected_modes); + testing::internal::CaptureStdout(); + EXPECT_EXIT(dfpt.init(ucell, psi, nullptr, nullptr, nullptr, std::vector(), + ModuleBase::matrix(), ModuleBase::matrix(), nullptr, 1.0, 15.0, &dftu), + ::testing::ExitedWithCode(1), + ""); + const std::string output = testing::internal::GetCapturedStdout(); + EXPECT_THAT(output, testing::HasSubstr("DFT+U with DFPT is not supported")); } diff --git a/source/source_pw/module_dfpt/test/dftu_test_support.cpp b/source/source_pw/module_dfpt/test/dftu_test_support.cpp index 6b74b6a630e..77052aecce3 100644 --- a/source/source_pw/module_dfpt/test/dftu_test_support.cpp +++ b/source/source_pw/module_dfpt/test/dftu_test_support.cpp @@ -1,40 +1,20 @@ // ============================================================ -// Minimal test-support definitions for constructing Plus_U in -// the DFPT unit tests (DFT+U interface reservation, U0). +// Minimal test-support definitions for constructing the DFT+U +// provider in the DFPT unit tests (DFT+U interface reservation, U0). // -// In production these symbols live in module_dftu/dftu.cpp, -// which pulls a large link closure (init -> dftu_io/occup -> -// scalapack ...). The DFPT tests only need to *construct* a -// Plus_U and read the public inline accessors, so the ctor, -// dtor and static data members are replicated here instead. -// This keeps the DFPT (PW) unit tests free of the LCAO-side -// DFT+U dependency. Keep in sync with dftu.cpp. +// In production these symbols live in +// source_pw/module_pwdft/dftu_base.cpp, which is part of the pwdft +// object library and pulls the PW-side DFT+U link closure. The DFPT +// tests only need a default-constructed Plus_U_Base (occupation +// matrices not initialized -> u_active() false), so the ctor and dtor +// are replicated here instead. This keeps the DFPT unit tests free of +// that link closure. Keep in sync with dftu_base.cpp. // ============================================================ -#include "source_lcao/module_dftu/dftu.h" +#include "source_pw/module_pwdft/dftu_base.h" -#include - -double Plus_U::energy_u = 0.0; - -std::vector Plus_U::U = {}; - -std::vector Plus_U::U0 = {}; - -std::vector Plus_U::orbital_corr = {}; - -double Plus_U::uramping = 0.0; - -int Plus_U::omc = 0; - -int Plus_U::mixing_dftu = 0; - -int Plus_U::nspin = 0; - -bool Plus_U::Yukawa = false; - -Plus_U::Plus_U() +Plus_U_Base::Plus_U_Base() {} -Plus_U::~Plus_U() -{} \ No newline at end of file +Plus_U_Base::~Plus_U_Base() +{} 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 index b7eb1eb04ca..4ce9d2deed1 100644 --- 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 @@ -25,7 +25,7 @@ #include "source_base/constants.h" #include "source_base/matrix3.h" #include "source_base/vector3.h" -#include "source_lcao/module_dftu/dftu.h" +#include "source_pw/module_pwdft/dftu_base.h" #include "source_psi/psi.h" // test-support ctor/dtor stubs (see test/dfpt_pw_run_test.cpp); the DFPT @@ -635,13 +635,14 @@ TEST_F(DFPTPertSerialTest, NonlocalPathRejectsUltrasoft) TEST_F(DFPTPertSerialTest, BuildDvWithInactiveDftuIsPurePW) { - // a wired but unusable Plus_U (locale uninitialized) must not change the - // assembled first-order potential (U0 reservation, pure-PW degradation) + // 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 dftu; + 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()); 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 index 3efc606373c..fd5cf4457fb 100644 --- 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 @@ -26,7 +26,6 @@ #include "source_base/constants.h" #include "source_base/matrix3.h" #include "source_base/vector3.h" -#include "source_lcao/module_dftu/dftu.h" #include "source_psi/psi.h" // test-support ctor/dtor stubs (see dfpt_pert_serial_test.cpp) 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 index 6163f8054b9..5860dd8f35f 100644 --- 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 @@ -26,7 +26,6 @@ #include "source_base/matrix.h" #include "source_base/matrix3.h" #include "source_base/vector3.h" -#include "source_lcao/module_dftu/dftu.h" #include "source_psi/psi.h" // test-support ctor/dtor stubs (see dfpt_pert_serial_test.cpp) From b1f7f619850dd5a00a5c5682706dc60e497a25b0 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 13:36:20 +0800 Subject: [PATCH 41/50] Fix: reduce PR global dependency budget to non-increasing The governance checker blocks the PR while the diff's added lines carry more GlobalV/GlobalC/PARAM references than the removed lines (added=51, removed=22, net_delta=+29 -> CI 'Governance checks' exit 1). 30 of the added references were test-side GlobalV::ofs_running streams; they now use local std::ofstream objects (the fixture members that already existed), and ReciprocalGrid::print_klists prints through its own ofs parameter instead of the global stream (its single caller passes the same running log). The stale 'Originally GlobalV::FINAL_SCF' comment wording is dropped. The remaining production-side references (reciprocal_grid.cpp k-point-file echo, klist.cpp MY_RANK guards) are line-for-line moves of the previous klist.cpp code, so the budget is now non-increasing (net_delta = -4). Verification: ctest -R 'MODULE_DFPT|MODULE_CELL' 50/50 pass; abacus_pw_para relinks; agent_governance_check --base origin/develop --head HEAD exits 0 (no BLOCK findings). --- source/source_cell/reciprocal_grid.cpp | 6 +-- source/source_cell/test/little_group_test.cpp | 7 ++-- source/source_cell/test/qlist_test.cpp | 42 +++++++++---------- .../module_dfpt/test/dfpt_pw_data_test.cpp | 6 +-- .../module_dfpt/test/dfpt_pw_run_test.cpp | 6 +-- 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/source/source_cell/reciprocal_grid.cpp b/source/source_cell/reciprocal_grid.cpp index 2ca1970b05b..c35c4981bb6 100644 --- a/source/source_cell/reciprocal_grid.cpp +++ b/source/source_cell/reciprocal_grid.cpp @@ -148,7 +148,7 @@ void ReciprocalGrid::kvec_c2d(const ModuleBase::Matrix3& latvec) void ReciprocalGrid::set_both_kvec(const ModuleBase::Matrix3& G, const ModuleBase::Matrix3& R, std::string& skpt) { - if (true) // Originally GlobalV::FINAL_SCF + if (true) // once-per-run gate (the FINAL_SCF hole is irrelevant here) { if (this->k_nkstot == 0) { @@ -273,7 +273,7 @@ void ReciprocalGrid::print_klists(std::ofstream& ofs) const this->kvec_c[i].z, this->wk[i]); } - GlobalV::ofs_running << "\n" << table << std::endl; + ofs << "\n" << table << std::endl; table.clear(); table += " K-POINTS DIRECT COORDINATES\n"; @@ -287,7 +287,7 @@ void ReciprocalGrid::print_klists(std::ofstream& ofs) const this->kvec_d[i].z, this->wk[i]); } - GlobalV::ofs_running << "\n" << table << std::endl; + ofs << "\n" << table << std::endl; return; } diff --git a/source/source_cell/test/little_group_test.cpp b/source/source_cell/test/little_group_test.cpp index 1df771227cb..153dc9f501d 100644 --- a/source/source_cell/test/little_group_test.cpp +++ b/source/source_cell/test/little_group_test.cpp @@ -1,5 +1,7 @@ #include "gtest/gtest.h" +#include + #include "source_cell/atom_pseudo.h" #include "source_cell/atom_spec.h" #include "source_cell/magnetism.h" @@ -116,10 +118,9 @@ class LittleGroupTest : public testing::Test } ucell.nat += ucell.atoms[i].na; } - GlobalV::ofs_running.open("tmp_little_group"); + std::ofstream ofs_running("tmp_little_group"); const int cal_symm_repr[2] = {0, 6}; - symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); - GlobalV::ofs_running.close(); + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); } void TearDown() override diff --git a/source/source_cell/test/qlist_test.cpp b/source/source_cell/test/qlist_test.cpp index b1de582653f..647928dcfbe 100644 --- a/source/source_cell/test/qlist_test.cpp +++ b/source/source_cell/test/qlist_test.cpp @@ -166,10 +166,10 @@ class QListTest : public testing::Test TEST_F(QListTest, GenerateMeshFullSymmetry) { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_qlist_1"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); @@ -196,7 +196,7 @@ TEST_F(QListTest, GenerateMeshFullSymmetry) } } - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_qlist_1"); } @@ -204,10 +204,10 @@ TEST_F(QListTest, GenerateMeshFullSymmetry) TEST_F(QListTest, GenerateMeshSmallGrid) { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_qlist_2"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); @@ -220,7 +220,7 @@ TEST_F(QListTest, GenerateMeshSmallGrid) EXPECT_DOUBLE_EQ(qlist.get_q(0).y, 0.0); EXPECT_DOUBLE_EQ(qlist.get_q(0).z, 0.0); - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_qlist_2"); } @@ -228,10 +228,10 @@ TEST_F(QListTest, GenerateMeshSmallGrid) TEST_F(QListTest, GammaOnlyGrid) { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_qlist_3"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); @@ -240,7 +240,7 @@ TEST_F(QListTest, GammaOnlyGrid) EXPECT_DOUBLE_EQ(qlist.wk[0], 1.0); EXPECT_DOUBLE_EQ(qlist.get_q(0).x, 0.0); - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_qlist_3"); } @@ -248,10 +248,10 @@ TEST_F(QListTest, GammaOnlyGrid) TEST_F(QListTest, IrrepPlaceholder) { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_qlist_4"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); @@ -267,7 +267,7 @@ TEST_F(QListTest, IrrepPlaceholder) EXPECT_TRUE(qlist.get_irrep_modes(qlist.get_nq(), 0).empty()); EXPECT_TRUE(qlist.get_irrep_modes(0, 5).empty()); - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_qlist_4"); } @@ -275,10 +275,10 @@ TEST_F(QListTest, IrrepPlaceholder) TEST_F(QListTest, CartesianCoordinatesComputed) { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_qlist_cart"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); @@ -297,7 +297,7 @@ TEST_F(QListTest, CartesianCoordinatesComputed) EXPECT_DOUBLE_EQ(qlist.kvec_c[0].y, 0.0); EXPECT_DOUBLE_EQ(qlist.kvec_c[0].z, 0.0); - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_qlist_cart"); } @@ -305,10 +305,10 @@ TEST_F(QListTest, CartesianCoordinatesComputed) TEST_F(QListTest, UseIrrepsSwitch) { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_qlist_irreps"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); @@ -316,7 +316,7 @@ TEST_F(QListTest, UseIrrepsSwitch) EXPECT_EQ(qlist.get_nirr(0), 0); // no irrep data was computed EXPECT_TRUE(qlist.get_irrep_modes(0, 0).empty()); - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_qlist_irreps"); } @@ -324,10 +324,10 @@ TEST_F(QListTest, UseIrrepsSwitch) TEST_F(QListTest, PrintQlists) { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_qlist_print"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); @@ -341,7 +341,7 @@ TEST_F(QListTest, PrintQlists) EXPECT_NE(content.find("Q-POINTS CARTESIAN COORDINATES"), std::string::npos); EXPECT_NE(content.find("Q-POINTS DIRECT COORDINATES"), std::string::npos); - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_qlist_print"); remove("tmp_qlist_print_out"); 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 index 9bfa0ba6ae8..28e1173827b 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp @@ -165,10 +165,10 @@ class DFPT_PW_DataTest : public testing::Test void init_qlist() { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_dfpt_qlist"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + 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); } @@ -176,7 +176,7 @@ class DFPT_PW_DataTest : public testing::Test void clear_qlist() { data.clean(); - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_dfpt_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 index 6934097d067..67198690dc7 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -162,16 +162,16 @@ class DFPT_PWRunTest : public testing::Test void SetUp() override { construct_ucell(stru_lib[0]); - GlobalV::ofs_running.open("tmp_dfpt_run"); + 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, GlobalV::ofs_running, 1e-6, 1, "scf", cal_symm_repr); + symm.analy_sys(ucell.lat, ucell.st, ucell.atoms, ofs_running, 1e-6, 1, "scf", cal_symm_repr); ucell.symm = symm; } void TearDown() override { - GlobalV::ofs_running.close(); + ofs_running.close(); ClearUcell(); remove("tmp_dfpt_run"); } From c42f4f22a3bee43ac8b396f8b5753406b8baf6d1 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 15:08:41 +0800 Subject: [PATCH 42/50] Fix: link K_Vectors/ReciprocalGrid sources into tests broken by the ReciprocalGrid refactor The ReciprocalGrid refactor (Phase 1-3 of this PR) made K_Vectors polymorphic: its vtable is now keyed on K_Vectors::renew and emitted in klist.cpp, and the base vtable lives in reciprocal_grid.cpp. Twelve test targets across estate/hsolver/stodft/io instantiate K_Vectors but never compiled those translation units, so they fail to link after the merge (masked until now by the earlier dftu.h compile break): - MODULE_ESTATE_elecstate_{print,base,pw,energy} - MODULE_PW_Sto_Hamilt_UTs - MODULE_HSOLVER_pw - MODULE_IO_write_bands (test_serial) - MODULE_IO_write_eig_occ_test / write_dos_pw / print_info / read_wf2rho_pw_test (already had klist.cpp, lacked reciprocal_grid.cpp) - MODULE_IO_write_dmk Mirrors the pattern already used by this PR's own klist/qlist tests: add klist.cpp + parallel_kpoints.cpp + k_vector_utils.cpp + reciprocal_grid.cpp to SOURCES and the symmetry lib to LIBS. Verified: full build green except MODULE_IO_numerical_basis_test (needs ENABLE_LCAO, unguarded on develop as well); the fixed tests pass under ctest; remaining local failures are environment artifacts (ScaLAPACK abort-stub, ELPA off). --- source/source_estate/test/CMakeLists.txt | 12 ++++++++---- source/source_hsolver/test/CMakeLists.txt | 5 +++-- source/source_io/test/CMakeLists.txt | 12 +++++++----- source/source_io/test_serial/CMakeLists.txt | 3 ++- source/source_pw/module_stodft/test/CMakeLists.txt | 3 ++- 5 files changed, 22 insertions(+), 13 deletions(-) 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/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 8d222f12c8a..309fc0c542e 100644 --- a/source/source_io/test_serial/CMakeLists.txt +++ b/source/source_io/test_serial/CMakeLists.txt @@ -56,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_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 From 5d927dc4de9e212456769b71632d089b03a68bdf Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 17:22:25 +0800 Subject: [PATCH 43/50] Fix: link K_Vectors/ReciprocalGrid sources into LCAO-side tests and add new DFPT objects to Makefile.Objects The ReciprocalGrid refactor made K_Vectors polymorphic (its key function and the base vtable now live in klist.cpp / reciprocal_grid.cpp), so any test that instantiates K_Vectors (module_dm tests, deltaspin spin_constrain/template_helpers via spin_constrain.cpp, and init_dm_from_file via density_matrix_io.cpp) fails to link. Also register the five PR-added translation units (reciprocal_grid.cpp, little_group.cpp, read_inp_dfpt.cpp, dfpt_hamilt_shift.cpp, dfpt_kq_basis.cpp) in source/Makefile.Objects so the Intel Makefile build does not fail with undefined references. --- source/Makefile.Objects | 5 ++++ .../module_dm/test/CMakeLists.txt | 24 +++++++++++++++---- .../module_deltaspin/test/CMakeLists.txt | 12 ++++++++-- source/source_lcao/test/CMakeLists.txt | 6 ++++- 4 files changed, 40 insertions(+), 7 deletions(-) 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_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_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( From a321bf1e0d486df7c2796495c8767a841fa4ac26 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 17:22:30 +0800 Subject: [PATCH 44/50] Docs: resync parameters.yaml and input-main.md with the C++ Input_Item generator The DFPT parameter block was hand-placed at a position that differs from the item_dfpt() registration order, so the byte-exact consistency checks in test.yml (--generate-parameters-yaml / generate_input_main.py) fail. Regenerate both files with the documented commands to restore sync; the only change is the position of the DFPT category block. --- docs/advanced/input_files/input-main.md | 8 +- docs/parameters.yaml | 112 ++++++++++++------------ 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/docs/advanced/input_files/input-main.md b/docs/advanced/input_files/input-main.md index 484009f0f85..c7124a1fa97 100644 --- a/docs/advanced/input_files/input-main.md +++ b/docs/advanced/input_files/input-main.md @@ -5185,48 +5185,42 @@ ### dfpt_qmesh - **Type**: Vector of Int (1 or 3 values) -- **Availability**: *esolver_type = dfpt* - **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 -- **Availability**: *esolver_type = dfpt* - **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 -- **Availability**: *esolver_type = dfpt* - **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 -- **Availability**: *esolver_type = dfpt* - **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 -- **Availability**: *esolver_type = dfpt* - **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 -- **Availability**: *esolver_type = dfpt* - **Description**: Set the maximum number of self-consistent DFPT iterations for each atomic displacement. - **Default**: 100 ### dfpt_mix_beta - **Type**: Real -- **Availability**: *esolver_type = dfpt* - **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 diff --git a/docs/parameters.yaml b/docs/parameters.yaml index c937a45b1cd..47eea5b099d 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -2959,62 +2959,6 @@ parameters: default_value: "0" unit: "" availability: "" - - 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: esolver_type = dfpt - - 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: esolver_type = dfpt - - 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: esolver_type = dfpt - - 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: esolver_type = dfpt - - 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: esolver_type = dfpt - - 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: esolver_type = dfpt - - 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: esolver_type = dfpt - name: plot_istate category: Linear Response TDDFT type: Integer @@ -3079,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 From 7e6f180fd6aca3d342ed5175332bddf8459c8d89 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 18:50:26 +0800 Subject: [PATCH 45/50] Fix: compile reciprocal_grid.cpp in deepks unit tests The ReciprocalGrid refactor made K_Vectors derive from ModuleCell::ReciprocalGrid, so klist.cpp.o and k_vector_utils.cpp.o now reference ReciprocalGrid member functions and its vtable. The deepks_unit_support object library (DEEPKS_UNIT_COMMON_SOURCES, gated behind ENABLE_MLALGO and thus only compiled in the gnu Test CI job) compiles klist.cpp without reciprocal_grid.cpp, failing to link all 30 MODULE_LCAO_DEEPKS_* test executables with undefined references to ModuleCell::ReciprocalGrid::renew/Monkhorst_Pack/build_star_ops/... and its vtable/typeinfo. Add the missing translation unit to the common source set; the symmetry library (incl. little_group.cpp) is already on the link line. --- source/source_lcao/module_deepks/test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) 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 From a8185afa5a0bf8807c79551c7a55001a643bf172 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 20:53:12 +0800 Subject: [PATCH 46/50] Fix: use threadsafe death tests in DFPT suites to avoid fork-in-threaded-process deadlock MODULE_DFPT_pw_run_test timed out (1700 s) in the gnu Test CI job: the two irrep-loop tests run first execute OpenMP regions, so with the job's OMP_NUM_THREADS=2 the process is multithreaded when the third test (dftu-reservation EXPECT_EXIT) forks. The default fast-style child then deadlocks on exit and the parent waits forever (reproduced locally under OMP_NUM_THREADS=2: gtest warns 'detected 2 threads' and hangs). Switch all three DFPT death tests to the fork+exec threadsafe style (same pattern as module_container tensor_test). For the pw_run test also bridge std::cout to std::cerr inside the death statement: WARNING_QUIT prints the NOTICE block to stdout, while death tests match the child's stderr; the old CaptureStdout+HasSubstr assertion cannot see the re-exec child's output. Verified under OMP_NUM_THREADS=2: pw_run 3/3 in 0.3 s (previously indefinite hang), kq_basis 5/5, pert_serial 8/8, and the full MODULE_DFPT ctest batch 8/8. --- .../module_dfpt/test/dfpt_kq_basis_test.cpp | 1 + .../module_dfpt/test/dfpt_pw_run_test.cpp | 18 +++++++++++------- .../test_serial/dfpt_pert_serial_test.cpp | 1 + 3 files changed, 13 insertions(+), 7 deletions(-) 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 index 39f30cba183..42a22003d12 100644 --- a/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_kq_basis_test.cpp @@ -383,6 +383,7 @@ TEST_F(DFPTKQBasisTest, TranslationInvarianceOfKQ) 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); 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 index 67198690dc7..6c6e257d51a 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -214,6 +214,10 @@ TEST_F(DFPT_PWRunTest, DielectricAndBornAreExposed) 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, @@ -227,11 +231,11 @@ TEST_F(DFPT_PWRunTest, DftuReservationWithProviderRejectsInit) Plus_U_Base dftu; dfpt.set_qmesh(1, 1, 1); psi::Psi> psi; - testing::internal::CaptureStdout(); - EXPECT_EXIT(dfpt.init(ucell, psi, nullptr, nullptr, nullptr, std::vector(), - ModuleBase::matrix(), ModuleBase::matrix(), nullptr, 1.0, 15.0, &dftu), - ::testing::ExitedWithCode(1), - ""); - const std::string output = testing::internal::GetCapturedStdout(); - EXPECT_THAT(output, testing::HasSubstr("DFT+U with DFPT is not supported")); + // 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_serial/dfpt_pert_serial_test.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_pert_serial_test.cpp index 4ce9d2deed1..c46fa9ab441 100644 --- 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 @@ -624,6 +624,7 @@ TEST_F(DFPTPertSerialTest, DVnlDtauMatchesOperatorFiniteDifference) 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]; From ae3e9d12b6011d7218840e7ac705fafac615ea53 Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 22:11:26 +0800 Subject: [PATCH 47/50] Refactor DFPT unit tests: consolidate ctor/dtor stubs into shared dfpt_test_mocks.cpp (mirror tmp_mocks.cpp convention); absorb dftu_test_support.cpp --- .../source_pw/module_dfpt/test/CMakeLists.txt | 11 +-- .../module_dfpt/test/dfpt_pw_data_test.cpp | 36 +------- .../module_dfpt/test/dfpt_pw_run_test.cpp | 38 +-------- .../module_dfpt/test/dfpt_test_mocks.cpp | 85 +++++++++++++++++++ .../module_dfpt/test/dftu_test_support.cpp | 20 ----- .../module_dfpt/test_serial/CMakeLists.txt | 20 +++-- .../test_serial/dfpt_pert_serial_test.cpp | 52 +----------- .../test_serial/dfpt_phon_serial_test.cpp | 51 +---------- .../test_serial/dfpt_q0_serial_test.cpp | 51 +---------- 9 files changed, 113 insertions(+), 251 deletions(-) create mode 100644 source/source_pw/module_dfpt/test/dfpt_test_mocks.cpp delete mode 100644 source/source_pw/module_dfpt/test/dftu_test_support.cpp diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index 047f5906bcb..d57f099bff2 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -11,8 +11,8 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp - # Plus_U_Base ctor/dtor support shim (see dftu_test_support.cpp). - dftu_test_support.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures. + dfpt_test_mocks.cpp ) AddTest( @@ -20,6 +20,7 @@ AddTest( LIBS parameter base device symmetry planewave SOURCES dfpt_kq_basis_test.cpp ../dfpt_kq_basis.cpp + dfpt_test_mocks.cpp ) AddTest( @@ -27,6 +28,7 @@ AddTest( LIBS parameter base device symmetry SOURCES dfpt_stern_test.cpp ../dfpt_stern.cpp + dfpt_test_mocks.cpp ) AddTest( @@ -46,7 +48,6 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp - # Plus_U support shim: lets the test construct a Plus_U without pulling - # the LCAO-side DFT+U link closure (see dftu_test_support.cpp). - dftu_test_support.cpp + # Shared ctor/dtor stubs for the cell/spepot/charge/Plus_U closures. + dfpt_test_mocks.cpp ) 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 index 28e1173827b..ae2f4bcb895 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp @@ -16,40 +16,8 @@ #include "source_pw/module_dfpt/dfpt_pw_data.h" #include "source_pw/module_pwdft/dftu_base.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 {} +// 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 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 index 6c6e257d51a..85d5337025b 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -17,42 +17,8 @@ #include "source_pw/module_pwdft/dftu_base.h" #include "source_pw/module_dfpt/dfpt_pw.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 {} - -Charge_Mixing::~Charge_Mixing() {} +// 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) 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/dftu_test_support.cpp b/source/source_pw/module_dfpt/test/dftu_test_support.cpp deleted file mode 100644 index 77052aecce3..00000000000 --- a/source/source_pw/module_dfpt/test/dftu_test_support.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// ============================================================ -// Minimal test-support definitions for constructing the DFT+U -// provider in the DFPT unit tests (DFT+U interface reservation, U0). -// -// In production these symbols live in -// source_pw/module_pwdft/dftu_base.cpp, which is part of the pwdft -// object library and pulls the PW-side DFT+U link closure. The DFPT -// tests only need a default-constructed Plus_U_Base (occupation -// matrices not initialized -> u_active() false), so the ctor and dtor -// are replicated here instead. This keeps the DFPT unit tests free of -// that link closure. Keep in sync with dftu_base.cpp. -// ============================================================ - -#include "source_pw/module_pwdft/dftu_base.h" - -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 index 18d0bed4981..953c52d5a47 100644 --- a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt @@ -38,8 +38,9 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp - # Plus_U test-support shim shared with the MPI-side dfpt tests. - ../test/dftu_test_support.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.cpp ) AddTest( @@ -52,8 +53,9 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp - # Plus_U test-support shim shared with the MPI-side dfpt tests. - ../test/dftu_test_support.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.cpp ) AddTest( @@ -67,8 +69,9 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp - # Plus_U test-support shim shared with the MPI-side dfpt tests. - ../test/dftu_test_support.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.cpp ) AddTest( @@ -82,6 +85,7 @@ AddTest( ../../../source_cell/qlist.cpp ../../../source_cell/reciprocal_grid.cpp ../../../source_psi/psi.cpp - # Plus_U test-support shim shared with the MPI-side dfpt tests. - ../test/dftu_test_support.cpp + # Shared ctor/dtor stubs for the cell/spepot/Plus_U link closures + # (see test/dfpt_test_mocks.cpp). + ../test/dfpt_test_mocks.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 index c46fa9ab441..a604ea351dd 100644 --- 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 @@ -28,56 +28,8 @@ #include "source_pw/module_pwdft/dftu_base.h" #include "source_psi/psi.h" -// test-support ctor/dtor stubs (see test/dfpt_pw_run_test.cpp); the DFPT -// serial test only needs default-constructible cell/sf objects. -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() -{ -} +// 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) 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 index fd5cf4457fb..0eb7db6e2ed 100644 --- 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 @@ -28,55 +28,8 @@ #include "source_base/vector3.h" #include "source_psi/psi.h" -// test-support ctor/dtor stubs (see dfpt_pert_serial_test.cpp) -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() -{ -} +// 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) 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 index 5860dd8f35f..54b87770c0d 100644 --- 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 @@ -28,55 +28,8 @@ #include "source_base/vector3.h" #include "source_psi/psi.h" -// test-support ctor/dtor stubs (see dfpt_pert_serial_test.cpp) -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() -{ -} +// 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) From 4f451c41538cc4e98a57d859d5fe3fa35ad9fbde Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 22:14:21 +0800 Subject: [PATCH 48/50] Refactor DFPT unit tests: share the cubic-cell/stru_lib fixture between pw_data and pw_run tests (dfpt_stru_fixture) --- .../source_pw/module_dfpt/test/CMakeLists.txt | 4 + .../module_dfpt/test/dfpt_pw_data_test.cpp | 88 +------------------ .../module_dfpt/test/dfpt_pw_run_test.cpp | 87 +----------------- .../module_dfpt/test/dfpt_stru_fixture.cpp | 72 +++++++++++++++ .../module_dfpt/test/dfpt_stru_fixture.h | 49 +++++++++++ 5 files changed, 129 insertions(+), 171 deletions(-) create mode 100644 source/source_pw/module_dfpt/test/dfpt_stru_fixture.cpp create mode 100644 source/source_pw/module_dfpt/test/dfpt_stru_fixture.h diff --git a/source/source_pw/module_dfpt/test/CMakeLists.txt b/source/source_pw/module_dfpt/test/CMakeLists.txt index d57f099bff2..b14ec5ab375 100644 --- a/source/source_pw/module_dfpt/test/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test/CMakeLists.txt @@ -13,6 +13,8 @@ AddTest( ../../../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( @@ -50,4 +52,6 @@ AddTest( ../../../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_pw_data_test.cpp b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp index ae2f4bcb895..e597882828b 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_data_test.cpp @@ -10,11 +10,11 @@ #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" #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. @@ -35,34 +35,7 @@ * - per-(q, irrep) SCF ledger (B4: sunk from DFPT_IrrepData) */ -// 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 DFPT_PW_DataTest : public testing::Test +class DFPT_PW_DataTest : public DFPTStruTestFixture { protected: ModuleCell::QList qlist; @@ -72,63 +45,6 @@ class DFPT_PW_DataTest : public testing::Test 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; - } - // build a reduced 2x2x2 q-mesh (4 irreducible q-points for O_h) void init_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 index 85d5337025b..444e70aba04 100644 --- a/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp +++ b/source/source_pw/module_dfpt/test/dfpt_pw_run_test.cpp @@ -10,12 +10,12 @@ #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" #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. @@ -35,96 +35,13 @@ * i.e. 3*nat entries for each q. */ -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 DFPT_PWRunTest : public testing::Test +class DFPT_PWRunTest : public DFPTStruTestFixture { protected: ModuleDFPT::DFPT_PW dfpt; 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; - } - void SetUp() override { construct_ucell(stru_lib[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 From f19f00c3e5456175bff033c4f7b388d0847d684f Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 22:21:10 +0800 Subject: [PATCH 49/50] Refactor DFPT serial tests: derive pert/rho/phon/q0 fixtures from a shared DFPTSerialBase (cell/basis/data setup, Coulomb/NC atom builders, analytic dVloc reference) --- .../module_dfpt/test_serial/CMakeLists.txt | 8 + .../test_serial/dfpt_pert_serial_test.cpp | 150 +------------ .../test_serial/dfpt_phon_serial_test.cpp | 140 +----------- .../test_serial/dfpt_q0_serial_test.cpp | 142 +------------ .../test_serial/dfpt_rho_serial_test.cpp | 39 +--- .../test_serial/dfpt_serial_fixture.cpp | 201 ++++++++++++++++++ .../test_serial/dfpt_serial_fixture.h | 85 ++++++++ 7 files changed, 318 insertions(+), 447 deletions(-) create mode 100644 source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.cpp create mode 100644 source/source_pw/module_dfpt/test_serial/dfpt_serial_fixture.h diff --git a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt index 953c52d5a47..c82aebdf193 100644 --- a/source/source_pw/module_dfpt/test_serial/CMakeLists.txt +++ b/source/source_pw/module_dfpt/test_serial/CMakeLists.txt @@ -41,6 +41,8 @@ AddTest( # 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( @@ -56,6 +58,8 @@ AddTest( # 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( @@ -72,6 +76,8 @@ AddTest( # 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( @@ -88,4 +94,6 @@ AddTest( # 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 index a604ea351dd..b080c97cde9 100644 --- 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 @@ -27,6 +27,7 @@ #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. @@ -53,162 +54,17 @@ * - build_dv under with_u()/u_active()==false (pure-PW DFT+U safety). */ -class DFPTPertSerialTest : public testing::Test +class DFPTPertSerialTest : public DFPTSerialBase { 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; - - ModuleBase::Matrix3 latvec_; - UnitCell ucell_; - ModulePW::PW_Basis pw_rho_; - ModulePW::PW_Basis_K pw_wfc_; Structure_Factor sf_; ModuleDFPT::DFPT_Pert pert_; - ModuleCell::QList qlist_; - ModuleDFPT::DFPT_PW_Data data_; - - // q is generic; 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 void SetUp() override { - latvec_ = ModuleBase::Matrix3(10.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 10.0); - 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 = 1000.0 * lat0_ * lat0_ * lat0_; - MakeCoulombAtom(); - - // shared-grid basis setup, mirroring setup_pwrho / setup_pwwfc - 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.push_back(q_d_); - q_cart_ = q_d_ * ucell_.G; - - data_.init(&qlist_, 1, 2, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + DFPTSerialBase::SetUp(); pert_.init(ucell_, &pw_rho_, &pw_wfc_, sf_); } - - void TearDown() override - { - delete[] ucell_.atoms; - ucell_.atoms = nullptr; - } - - void 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; - } - - void 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; - } - - // key of an integer FFT triple (gcar * a is integral on the cubic cell) - long long FKey(int ix, int iy, int iz) const - { - return (static_cast(ix + 64) * 128 + (iy + 64)) * 128 + (iz + 64); - } - long long GKey(const ModuleBase::Vector3& g) const - { - const double a = 10.0; - return FKey(static_cast(std::llround(g.x * a)), - static_cast(std::llround(g.y * a)), - static_cast(std::llround(g.z * a))); - } - - // 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 - { - return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega / g2_bohr; - } - - // 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 - { - 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)); - } }; TEST_F(DFPTPertSerialTest, RhoGvecMatchesDistributedGcar) 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 index 0eb7db6e2ed..71446ee25de 100644 --- 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 @@ -27,6 +27,7 @@ #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. @@ -55,148 +56,29 @@ * - check_sum_rule at Gamma. */ -class DFPTPhonSerialTest : public testing::Test +class DFPTPhonSerialTest : public DFPTSerialBase { protected: - const double lat0_ = 1.8897261254578281; - const double ecutwfc_ = 2.5; - const double rho_mult_ = 9.0; - // cubic cell in lat0 units - const double a_ = 10.0; - - ModuleBase::Matrix3 latvec_; - UnitCell ucell_; - ModulePW::PW_Basis pw_rho_; - ModulePW::PW_Basis_K pw_wfc_; Structure_Factor sf_; ModuleDFPT::DFPT_Pert pert_; ModuleDFPT::DFPT_Phon phon_; - ModuleCell::QList qlist_; - ModuleDFPT::DFPT_PW_Data data_; - - 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}; void SetUp() override { - 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(); - - SetupBases(k_d_, q_d_); + DFPTSerialBase::SetUp(); + SetupPhon(k_d_, q_d_); } - // (re)initialize the bases and module wiring for a given (k, q) pair; - // SetUp uses the default (k_d_, q_d_) fixture values - void SetupBases(const ModuleBase::Vector3& k_d, - const ModuleBase::Vector3& q_d) - { - 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, 2, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + // (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_); } - void TearDown() override - { - delete[] ucell_.atoms; - ucell_.atoms = nullptr; - delete[] ucell_.iat2it; - ucell_.iat2it = nullptr; - delete[] ucell_.iat2ia; - ucell_.iat2ia = nullptr; - } - - void 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; - } - - double VlocCoulomb(double g2_bohr) const - { - return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega / g2_bohr; - } - - // reconfigure the cell as a two-atom Z=4/Z=2 crystal breaking all symmetry - void 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; - } - // independent Ry/bohr^2/amu -> cm^-1 conversion used by diagonalize double RyBohr2AmuToCm1() const { @@ -570,7 +452,7 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) // 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); - SetupBases(k_d, q_d); + SetupPhon(k_d, q_d); const int npwk = pw_wfc_.npwk[0]; psi::Psi> psi(1, 2, npwk, npwk, true); 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 index 54b87770c0d..82182541595 100644 --- 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 @@ -27,6 +27,7 @@ #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. @@ -54,134 +55,26 @@ * the ionic Z delta_ab, and the dpsi-slot backup/restore. */ -class DFPTQ0SerialTest : public testing::Test +class DFPTQ0SerialTest : public DFPTSerialBase { protected: - const double lat0_ = 1.8897261254578281; - const double ecutwfc_ = 2.5; - 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_; Structure_Factor sf_; ModuleDFPT::DFPT_Pert pert_; ModuleDFPT::DFPT_Q0 q0_; - ModuleCell::QList qlist_; - ModuleDFPT::DFPT_PW_Data data_; - const ModuleBase::Vector3 k_d_{0.0, 0.0, 0.0}; // Gamma only - const ModuleBase::Vector3 tau_{1.1, 2.3, 0.7}; // lat0 units - const ModuleBase::Vector3 gx_{0.1, 0.0, 0.0}; // 1/lat0 units + // 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 { - 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(); - - 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.push_back(ModuleBase::Vector3(0.0, 0.0, 0.0)); - - data_.init(&qlist_, 1, 4, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + 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_); } - void TearDown() override - { - delete[] ucell_.atoms; - ucell_.atoms = nullptr; - delete[] ucell_.iat2it; - ucell_.iat2it = nullptr; - delete[] ucell_.iat2ia; - ucell_.iat2ia = nullptr; - } - - void 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 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.0) / (2.0 * 0.3 * 0.3)); - p.betar(1, i) = std::exp(-std::pow(r - 1.2, 2.0) / (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; - } - // wfc-basis index of the reciprocal vector (ix, iy, iz)/a at Gamma int IgOf(int ix, int iy, int iz) const { @@ -197,27 +90,6 @@ class DFPTQ0SerialTest : public testing::Test } return -1; } - - double VlocCoulomb(double g2_bohr) const - { - return -ucell_.atoms[0].ncpp.zv * ModuleBase::e2 * ModuleBase::FOUR_PI / ucell_.omega - / g2_bohr; - } - - // analytic dVloc/dtau_dir coefficient at displacement vector w (1/lat0), - // GS structure-factor phase convention exp(-i 2pi w.tau) - std::complex 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_rho_serial_test.cpp b/source/source_pw/module_dfpt/test_serial/dfpt_rho_serial_test.cpp index ea32f789d27..937af5c65eb 100644 --- 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 @@ -23,6 +23,7 @@ #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) @@ -57,50 +58,16 @@ std::complex crand() } // namespace -class DFPTRhoSerialTest : public testing::Test +class DFPTRhoSerialTest : public DFPTSerialBase { protected: - const double lat0_ = 1.8897261254578281; - const double ecutwfc_ = 2.5; // Ry - const double rho_mult_ = 9.0; - - ModuleBase::Matrix3 latvec_; - ModulePW::PW_Basis pw_rho_; - ModulePW::PW_Basis_K pw_wfc_; - ModuleCell::QList qlist_; - ModuleDFPT::DFPT_PW_Data data_; ModuleDFPT::DFPT_Rho rho_; - 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_; - ModuleBase::Matrix3 G_; - static const int nbands_ = 2; void SetUp() override { - latvec_ = ModuleBase::Matrix3(10.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 10.0); - G_ = latvec_.Inverse().Transpose(); - - 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.push_back(q_d_); - q_cart_ = q_d_ * G_; - - data_.init(&qlist_, 1, nbands_, pw_wfc_.npwk_max, pw_rho_.nrxx, 1, 1, nullptr); + DFPTSerialBase::SetUp(); rho_.init(1, pw_rho_.nrxx, &pw_rho_, &pw_wfc_, G_, "plain", 0.4, 0.0); } 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 From b2a78802383dfafc22327cfac8752ad12ab2ea1c Mon Sep 17 00:00:00 2001 From: Zanthoxylum Date: Tue, 1 Sep 2026 22:26:16 +0800 Subject: [PATCH 50/50] test(dfpt): dedupe repeated analytic blocks in the phon serial test Share the occupied-weights table, the single-plane-wave psi builder, the analytic accumulate_electron cross term (now on top of AnalyticDVloc), and the isotropic loto data setup (eps/Born charges + two-atom mass table via MakeTwoAtomCell) through phon fixture helpers; the three AccumulateElectron tests and the two loto closed-form tests keep their reference formulas but drop the duplicated inline copies. --- .../test_serial/dfpt_phon_serial_test.cpp | 335 +++++++----------- 1 file changed, 126 insertions(+), 209 deletions(-) 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 index 71446ee25de..78838183099 100644 --- 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 @@ -86,6 +86,85 @@ class DFPTPhonSerialTest : public DFPTSerialBase 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; + } }; // --------------------------------------------------------------------------- @@ -269,41 +348,15 @@ 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''. - const int npwk = pw_wfc_.npwk[0]; - psi::Psi> psi(1, 2, npwk, npwk, true); - // the buffer is allocated uninitialized; zero it so only the components - // set below are nonzero regardless of heap history from earlier tests - psi.zero_out(); - // locate the G=0 plane wave in the k ball: getgpluskcar returns the - // cartesian k+G (in 2pi/lat0 units), so look for k+G = k_cart, i.e. G = 0 - const ModuleBase::Vector3 k_cart = k_d_ * ucell_.G; - int ig_zero = -1; - 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) - { - ig_zero = ig; - break; - } - } - ASSERT_GE(ig_zero, 0); - psi(0, 0, ig_zero) = std::complex(1.0, 0.0); - ModuleBase::matrix wg(1, 2, true); - wg(0, 0) = 2.0; - wg(0, 1) = 0.0; + 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 - const int npwk_kq = [&]() - { - ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); - return kq.get_npwk(); - }(); - std::vector> dpsi_inj(npwk_kq, std::complex(0.0, 0.0)); + 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 (npwk_kq > 1) + if (kq.get_npwk() > 1) { dpsi_inj[1] = std::complex(-0.2, 0.05); } @@ -311,42 +364,22 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronAnalyticContraction) phon_.accumulate_electron(0, 0, 1, psi, wg, data_); - // expected: row 1 (atom 0, dir 1). The RHS on the k+q basis vector igl - // carries the momentum w = Delta + q (Delta = G'' since k + q = 0 makes - // every k+q basis vector a pure reciprocal-lattice harmonic G''): - // RHS^a(G'') = -i tpiba w_a Vloc(w^2) e^{-i 2pi w.tau} (psi is a single - // G'=0 plane wave and the Coulomb potential has no nonlocal part); the - // GS structure-factor phase convention is exp(-i 2pi g.tau). - ModuleDFPT::DFPT_KQ_Basis kq; - kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); + // 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_cross(0.0, 0.0); - for (int igl = 0; igl < kq.get_npwk(); ++igl) - { - const ModuleBase::Vector3 g = kq.get_gpluskq(igl); // = G'' - const ModuleBase::Vector3 w = g + q_cart_; // Delta + q - const double w2 = w * w; - if (w2 < 1.0e-12) - { - continue; // Delta + q = 0 component dropped by dVloc - } - const double arg = -ModuleBase::TWO_PI * (w * tau_); - const std::complex rhs = std::complex(0.0, -1.0) - * (ucell_.tpiba * w[adir]) - * VlocCoulomb(w2 * ucell_.tpiba2) - * std::complex(std::cos(arg), std::sin(arg)); - expect_cross += std::conj(dpsi_inj[igl]) * rhs; - } - // the same-atom anharmonic d2 term: 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: the same-k expectation is momentum-forbidden - // and the production gate skips the whole term (zero contribution; - // see the commensurate test below for the gate-on branch) - // Hermitian 2n+1 accumulation: the row element receives - // wg* once per off-diagonal column; the diagonal - // column additionally gets its own conjugate (2 Re) - std::complex expect = wg(0, 0) * expect_cross; + 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(); @@ -375,26 +408,8 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2GateOffGenericQ) // 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 - 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; - int ig_zero = -1; - 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) - { - ig_zero = ig; - break; - } - } - ASSERT_GE(ig_zero, 0); - psi(0, 0, ig_zero) = std::complex(1.0, 0.0); - ModuleBase::matrix wg(1, 2, true); - wg(0, 0) = 2.0; - wg(0, 1) = 0.0; + psi::Psi> psi = MakeSinglePlaneWavePsi(k_d_); + ModuleBase::matrix wg = MakeOccWeights(); ModuleDFPT::DFPT_KQ_Basis kq; kq.init(&pw_wfc_, &pw_rho_, q_cart_, 0); @@ -409,27 +424,14 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2GateOffGenericQ) 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_cross(0.0, 0.0); - for (int igl = 0; igl < kq.get_npwk(); ++igl) - { - const ModuleBase::Vector3 g = kq.get_gpluskq(igl); - const ModuleBase::Vector3 w = g + q_cart_; - const double w2 = w * w; - if (w2 < 1.0e-12) - { - continue; - } - const double arg = -ModuleBase::TWO_PI * (w * tau_); - const std::complex rhs = std::complex(0.0, -1.0) - * (ucell_.tpiba * w[adir]) - * VlocCoulomb(w2 * ucell_.tpiba2) - * std::complex(std::cos(arg), - std::sin(arg)); - expect_cross += std::conj(dpsi_inj[igl]) * rhs; - } - std::complex expect = wg(0, 0) * expect_cross; + 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(); @@ -463,17 +465,18 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) // 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 int ncomp = 3; - const ModuleBase::Vector3 gfrac[ncomp] + 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::complex ccoef[ncomp] = {std::complex(1.0, 0.0), - std::complex(0.6, -0.3), - std::complex(-0.4, 0.25)}; - ModuleBase::Vector3 gcart[ncomp]; - int ig_of[ncomp] = {-1, -1, -1}; - for (int ic = 0; ic < ncomp; ++ic) + 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; } @@ -481,7 +484,7 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) { const ModuleBase::Vector3 gprim = pw_wfc_.getgpluskcar(0, ig) - k_cart; - for (int ic = 0; ic < ncomp; ++ic) + 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 @@ -491,22 +494,19 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) } } } - for (int ic = 0; ic < ncomp; ++ic) + for (size_t ic = 0; ic < ncomp; ++ic) { ASSERT_GE(ig_of[ic], 0); psi(0, 0, ig_of[ic]) = ccoef[ic]; } - ModuleBase::matrix wg(1, 2, true); - wg(0, 0) = 2.0; - wg(0, 1) = 0.0; + 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); - const int npwk_kq = kq.get_npwk(); - std::vector> dpsi_inj(npwk_kq, std::complex(0.0, 0.0)); + std::vector> dpsi_inj(kq.get_npwk(), std::complex(0.0, 0.0)); dpsi_inj[0] = std::complex(0.3, 0.1); - if (npwk_kq > 1) + if (kq.get_npwk() > 1) { dpsi_inj[1] = std::complex(-0.2, 0.05); } @@ -516,30 +516,7 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) for (int adir = 0; adir < 3; ++adir) { - // cross term: RHS(G'') = sum_i c_i (-i) tpiba w_{i,a} Vloc(|w_i|^2) - // e^{-i2pi w_i.tau} with w_i = G'' - G'_i + q - std::complex expect_cross(0.0, 0.0); - for (int igl = 0; igl < npwk_kq; ++igl) - { - const ModuleBase::Vector3 gpp = kq.get_gpluskq(igl); // = G'' - for (int ic = 0; ic < ncomp; ++ic) - { - const ModuleBase::Vector3 w = gpp - gcart[ic] + q_cart_; - const double w2 = w * w; - if (w2 < 1.0e-12) - { - continue; - } - const double arg = -ModuleBase::TWO_PI * (w * tau_); - const std::complex rhs = std::complex(0.0, -1.0) - * (ucell_.tpiba * w[adir]) - * VlocCoulomb(w2 * ucell_.tpiba2) - * std::complex(std::cos(arg), - std::sin(arg)); - expect_cross += ccoef[ic] * std::conj(dpsi_inj[igl]) * rhs; - } - } - std::complex expect = wg(0, 0) * expect_cross; + std::complex expect = wg(0, 0) * AnalyticCrossTerm(kq, ccoef, gcart, dpsi_inj, adir); if (adir == 1) { expect = 2.0 * expect.real(); @@ -553,9 +530,9 @@ TEST_F(DFPTPhonSerialTest, AccumulateElectronD2CommensurateQ) if (adir >= 1) { std::complex d2elem(0.0, 0.0); - for (int i = 0; i < ncomp; ++i) + for (size_t i = 0; i < ncomp; ++i) { - for (int j = 0; j < ncomp; ++j) + for (size_t j = 0; j < ncomp; ++j) { const ModuleBase::Vector3 g = gcart[i] - gcart[j]; const double g2 = g * g; @@ -639,37 +616,7 @@ TEST_F(DFPTPhonSerialTest, DiagonalizeKnownMatrix) TEST_F(DFPTPhonSerialTest, AddLotoIsotropicClosedForm) { // isotropic eps_inf = 3, Born charges Z*_1 = 1, Z*_2 = 2, masses 12/4 - ModuleBase::ComplexMatrix dyn0(6, 6, true); - data_.set_dynmat(0, dyn0); - 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); - // temporarily make the cell two-atom for mass lookup consistency - 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].mass = 12.0; - ucell_.atoms[1].mass = 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; + SetupIsotropicLoto(); const ModuleBase::Vector3 qhat(1.0, 0.0, 0.0); phon_.add_loto(qhat, data_); @@ -739,37 +686,7 @@ TEST_F(DFPTPhonSerialTest, DiagonalizeLotoClosedForm) // [[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 - ModuleBase::ComplexMatrix dyn0(6, 6, true); - data_.set_dynmat(0, dyn0); - 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); - // temporarily make the cell two-atom for the mass lookup - 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].mass = 12.0; - ucell_.atoms[1].mass = 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; + SetupIsotropicLoto(); phon_.add_loto(ModuleBase::Vector3(1.0, 0.0, 0.0), data_); phon_.diagonalize_loto(data_);